diff options
583 files changed, 15845 insertions, 9834 deletions
diff --git a/api/Android.bp b/api/Android.bp index cd1997c8bf97..6a04f0d76bf9 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -368,8 +368,6 @@ stubs_defaults { "--hide CallbackInterface", // Disable HiddenSuperclass, as Metalava handles this fine (it should be hidden by default) "--hide HiddenSuperclass", - "--hide-package android.audio.policy.configuration.V7_0", - "--hide-package com.android.server", "--manifest $(location :frameworks-base-core-AndroidManifest.xml)", ], filter_packages: packages_to_document, diff --git a/core/api/current.txt b/core/api/current.txt index 1a930c50eaa8..9ad8dd3ab9de 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -1058,7 +1058,7 @@ package android { field public static final int label = 16842753; // 0x1010001 field public static final int labelFor = 16843718; // 0x10103c6 field @Deprecated public static final int labelTextSize = 16843317; // 0x1010235 - field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") public static final int languageSettingsActivity; + field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final int languageSettingsActivity; field public static final int languageTag = 16844040; // 0x1010508 field public static final int largeHeap = 16843610; // 0x101035a field public static final int largeScreens = 16843398; // 0x1010286 @@ -6844,6 +6844,9 @@ package android.app { method public android.app.Notification.MessagingStyle.Message setData(String, android.net.Uri); } + @FlaggedApi("android.app.api_rich_ongoing") public abstract static class Notification.RichOngoingStyle extends android.app.Notification.Style { + } + public abstract static class Notification.Style { ctor @Deprecated public Notification.Style(); method public android.app.Notification build(); @@ -11603,6 +11606,7 @@ package android.content { method public static android.content.IntentSender readIntentSenderOrNullFromParcel(android.os.Parcel); method public void sendIntent(android.content.Context, int, android.content.Intent, android.content.IntentSender.OnFinished, android.os.Handler) throws android.content.IntentSender.SendIntentException; method public void sendIntent(android.content.Context, int, android.content.Intent, android.content.IntentSender.OnFinished, android.os.Handler, String) throws android.content.IntentSender.SendIntentException; + method @FlaggedApi("com.android.window.flags.bal_send_intent_with_options") public void sendIntent(@Nullable android.content.Context, int, @Nullable android.content.Intent, @Nullable String, @Nullable android.os.Bundle, @Nullable java.util.concurrent.Executor, @Nullable android.content.IntentSender.OnFinished) throws android.content.IntentSender.SendIntentException; method public static void writeIntentSenderOrNullToParcel(android.content.IntentSender, android.os.Parcel); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.content.IntentSender> CREATOR; @@ -12043,6 +12047,7 @@ package android.content.pm { field public static final int COLOR_MODE_DEFAULT = 0; // 0x0 field public static final int COLOR_MODE_HDR = 2; // 0x2 field public static final int COLOR_MODE_WIDE_COLOR_GAMUT = 1; // 0x1 + field @FlaggedApi("android.content.res.handle_all_config_changes") public static final int CONFIG_ASSETS_PATHS = -2147483648; // 0x80000000 field public static final int CONFIG_COLOR_MODE = 16384; // 0x4000 field public static final int CONFIG_DENSITY = 4096; // 0x1000 field public static final int CONFIG_FONT_SCALE = 1073741824; // 0x40000000 @@ -12056,6 +12061,7 @@ package android.content.pm { field public static final int CONFIG_MNC = 2; // 0x2 field public static final int CONFIG_NAVIGATION = 64; // 0x40 field public static final int CONFIG_ORIENTATION = 128; // 0x80 + field @FlaggedApi("android.content.res.handle_all_config_changes") public static final int CONFIG_RESOURCES_UNUSED = 134217728; // 0x8000000 field public static final int CONFIG_SCREEN_LAYOUT = 256; // 0x100 field public static final int CONFIG_SCREEN_SIZE = 1024; // 0x400 field public static final int CONFIG_SMALLEST_SCREEN_SIZE = 2048; // 0x800 @@ -56332,7 +56338,7 @@ package android.view.inputmethod { public final class InputMethodInfo implements android.os.Parcelable { ctor public InputMethodInfo(android.content.Context, android.content.pm.ResolveInfo) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; ctor public InputMethodInfo(String, String, CharSequence, String); - method @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") @Nullable public android.content.Intent createImeLanguageSettingsActivityIntent(); + method @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") @Nullable public android.content.Intent createImeLanguageSettingsActivityIntent(); method @Nullable public android.content.Intent createStylusHandwritingSettingsActivityIntent(); method public int describeContents(); method public void dump(android.util.Printer, String); @@ -56353,7 +56359,7 @@ package android.view.inputmethod { method public boolean supportsStylusHandwriting(); method public boolean suppressesSpellChecker(); method public void writeToParcel(android.os.Parcel, int); - field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") public static final String ACTION_IME_LANGUAGE_SETTINGS = "android.view.inputmethod.action.IME_LANGUAGE_SETTINGS"; + field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final String ACTION_IME_LANGUAGE_SETTINGS = "android.view.inputmethod.action.IME_LANGUAGE_SETTINGS"; field public static final String ACTION_STYLUS_HANDWRITING_SETTINGS = "android.view.inputmethod.action.STYLUS_HANDWRITING_SETTINGS"; field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.InputMethodInfo> CREATOR; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index b384326201fc..36b1eaba89df 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -30,6 +30,7 @@ import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE; import static android.content.ContentResolver.DEPRECATE_DATA_COLUMNS; import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX; +import static android.content.pm.ActivityInfo.CONFIG_RESOURCES_UNUSED; import static android.content.res.Configuration.UI_MODE_TYPE_DESK; import static android.content.res.Configuration.UI_MODE_TYPE_MASK; import static android.view.Display.DEFAULT_DISPLAY; @@ -41,7 +42,6 @@ import static android.window.ConfigurationHelper.shouldUpdateResources; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL; import static com.android.sdksandbox.flags.Flags.sandboxActivitySdkBasedContext; -import static com.android.window.flags.Flags.activityWindowInfoFlag; import android.annotation.NonNull; import android.annotation.Nullable; @@ -6520,6 +6520,13 @@ public final class ActivityThread extends ClientTransactionHandler return true; } + if (android.content.res.Flags.handleAllConfigChanges()) { + if ((handledConfigChanges & CONFIG_RESOURCES_UNUSED) != 0) { + // Report the change if activities claim they do not use resources at all. + return true; + } + } + final int diffWithBucket = SizeConfigurationBuckets.filterDiff(publicDiff, currentConfig, newConfig, sizeBuckets); // Compare to the diff which filter the change without crossing size buckets with @@ -6846,9 +6853,6 @@ public final class ActivityThread extends ClientTransactionHandler } private void handleActivityWindowInfoChanged(@NonNull ActivityClientRecord r) { - if (!activityWindowInfoFlag()) { - return; - } if (r.mActivityWindowInfo.equals(r.mLastReportedActivityWindowInfo)) { return; } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index db979a5dd30b..ef09dc45d423 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -10979,6 +10979,18 @@ public class Notification implements Parcelable } /** + * An object that can apply a rich ongoing notification style to a {@link Notification.Builder} + * object. + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public abstract static class RichOngoingStyle extends Notification.Style { + /** + * @hide + */ + public RichOngoingStyle() {} + } + + /** * Notification style for custom views that are decorated by the system * * <p>Instead of providing a notification that is completely custom, a developer can set this diff --git a/core/java/android/app/servertransaction/ClientTransactionListenerController.java b/core/java/android/app/servertransaction/ClientTransactionListenerController.java index 0c1e7a32ae5e..c281533ee299 100644 --- a/core/java/android/app/servertransaction/ClientTransactionListenerController.java +++ b/core/java/android/app/servertransaction/ClientTransactionListenerController.java @@ -19,8 +19,6 @@ package android.app.servertransaction; import static android.app.WindowConfiguration.areConfigurationsEqualForDisplay; import static android.view.Display.INVALID_DISPLAY; -import static com.android.window.flags.Flags.activityWindowInfoFlag; - import static java.util.Objects.requireNonNull; import android.annotation.NonNull; @@ -102,9 +100,6 @@ public class ClientTransactionListenerController { */ public void registerActivityWindowInfoChangedListener( @NonNull BiConsumer<IBinder, ActivityWindowInfo> listener) { - if (!activityWindowInfoFlag()) { - return; - } synchronized (mLock) { mActivityWindowInfoChangedListeners.add(listener); } @@ -116,9 +111,6 @@ public class ClientTransactionListenerController { */ public void unregisterActivityWindowInfoChangedListener( @NonNull BiConsumer<IBinder, ActivityWindowInfo> listener) { - if (!activityWindowInfoFlag()) { - return; - } synchronized (mLock) { mActivityWindowInfoChangedListeners.remove(listener); } @@ -130,9 +122,6 @@ public class ClientTransactionListenerController { */ public void onActivityWindowInfoChanged(@NonNull IBinder activityToken, @NonNull ActivityWindowInfo activityWindowInfo) { - if (!activityWindowInfoFlag()) { - return; - } final Object[] activityWindowInfoChangedListeners; synchronized (mLock) { if (mActivityWindowInfoChangedListeners.isEmpty()) { diff --git a/core/java/android/audio/policy/configuration/V7_0/package-info.java b/core/java/android/audio/policy/configuration/V7_0/package-info.java new file mode 100644 index 000000000000..8f7425fc2b5b --- /dev/null +++ b/core/java/android/audio/policy/configuration/V7_0/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Hide the android.audio.policy.configuration.V7_0 API as that is managed + * separately. + * + * @hide + */ +package android.audio.policy.configuration.V7_0; diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index d05d23c336c2..55278f617ba9 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -32,6 +32,13 @@ flag { } flag { + namespace: "virtual_devices" + name: "media_projection_keyguard_restrictions" + description: "Auto-stop MP when the device locks" + bug: "348335290" +} + +flag { namespace: "virtual_devices" name: "virtual_display_insets" description: "APIs for specifying virtual display insets (via cutout)" diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 87fb84331af3..385d6cfd14a0 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -61,6 +61,7 @@ import android.os.storage.StorageManager; import android.permission.PermissionCheckerManager; import android.provider.MediaStore; import android.text.TextUtils; +import android.util.ArraySet; import android.util.Log; import android.util.SparseBooleanArray; @@ -486,6 +487,8 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall validateIncomingAuthority(authority); int numOperations = operations.size(); final int[] userIds = new int[numOperations]; + final ArraySet<String> readPermissions = new ArraySet<String>(); + final ArraySet<String> writePermissions = new ArraySet<String>(); for (int i = 0; i < numOperations; i++) { ContentProviderOperation operation = operations.get(i); Uri uri = operation.getUri(); @@ -499,17 +502,19 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall } final AttributionSource accessAttributionSource = attributionSource; - if (operation.isReadOperation()) { + if (operation.isReadOperation() && !readPermissions.contains(uri.toString())) { if (enforceReadPermission(accessAttributionSource, uri) != PermissionChecker.PERMISSION_GRANTED) { throw new OperationApplicationException("App op not allowed", 0); } + readPermissions.add(uri.toString()); } - if (operation.isWriteOperation()) { + if (operation.isWriteOperation() && !writePermissions.contains(uri.toString())) { if (enforceWritePermission(accessAttributionSource, uri) != PermissionChecker.PERMISSION_GRANTED) { throw new OperationApplicationException("App op not allowed", 0); } + writePermissions.add(uri.toString()); } } traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "applyBatch: ", authority); @@ -2774,6 +2779,10 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall + " provider from user:" + mContext.getUserId()); } if (userId != UserHandle.USER_CURRENT + // getUserIdFromAuthority can return USER_NULL when can't cast the userId to + // an int, which can cause high volume of binder calls. + && (!android.multiuser.Flags.fixGetUserPropertyCache() + || userId != UserHandle.USER_NULL) && userId != mContext.getUserId() // Since userId specified in content uri, the provider userId would be // determined from it. diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java index 2e252c12c51d..32d1964dd4f0 100644 --- a/core/java/android/content/IntentSender.java +++ b/core/java/android/content/IntentSender.java @@ -16,6 +16,7 @@ package android.content; +import android.annotation.FlaggedApi; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManager.PendingIntentInfo; @@ -32,6 +33,10 @@ import android.os.RemoteException; import android.os.UserHandle; import android.util.AndroidException; +import com.android.window.flags.Flags; + +import java.util.concurrent.Executor; + /** * A description of an Intent and target action to perform with it. * The returned object can be @@ -114,15 +119,15 @@ public class IntentSender implements Parcelable { implements Runnable { private final IntentSender mIntentSender; private final OnFinished mWho; - private final Handler mHandler; + private final Executor mExecutor; private Intent mIntent; private int mResultCode; private String mResultData; private Bundle mResultExtras; - FinishedDispatcher(IntentSender pi, OnFinished who, Handler handler) { + FinishedDispatcher(IntentSender pi, OnFinished who, Executor executor) { mIntentSender = pi; mWho = who; - mHandler = handler; + mExecutor = executor; } public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean serialized, boolean sticky, int sendingUser) { @@ -130,10 +135,10 @@ public class IntentSender implements Parcelable { mResultCode = resultCode; mResultData = data; mResultExtras = extras; - if (mHandler == null) { + if (mExecutor == null) { run(); } else { - mHandler.post(this); + mExecutor.execute(this); } } public void run() { @@ -147,16 +152,16 @@ public class IntentSender implements Parcelable { * caller to specify information about the Intent to use and be notified * when the send has completed. * - * @param context The Context of the caller. This may be null if - * <var>intent</var> is also null. + * @param context The Context of the caller. This may be {@code null} if + * <var>intent</var> is also {@code null}. * @param code Result code to supply back to the IntentSender's target. * @param intent Additional Intent data. See {@link Intent#fillIn * Intent.fillIn()} for information on how this is applied to the - * original Intent. Use null to not modify the original Intent. + * original Intent. Use {@code null} to not modify the original Intent. * @param onFinished The object to call back on when the send has - * completed, or null for no callback. + * completed, or {@code null} for no callback. * @param handler Handler identifying the thread on which the callback - * should happen. If null, the callback will happen from the thread + * should happen. If {@code null}, the callback will happen from the thread * pool of the process. * * @@ -165,8 +170,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, - SEND_INTENT_DEFAULT_OPTIONS); + sendIntent(context, code, intent, null, SEND_INTENT_DEFAULT_OPTIONS, + handler == null ? null : handler::post, onFinished); } /** @@ -174,22 +179,22 @@ public class IntentSender implements Parcelable { * caller to specify information about the Intent to use and be notified * when the send has completed. * - * @param context The Context of the caller. This may be null if - * <var>intent</var> is also null. + * @param context The Context of the caller. This may be {@code null} if + * <var>intent</var> is also {@code null}. * @param code Result code to supply back to the IntentSender's target. * @param intent Additional Intent data. See {@link Intent#fillIn * Intent.fillIn()} for information on how this is applied to the - * original Intent. Use null to not modify the original Intent. + * original Intent. Use {@code null} to not modify the original Intent. * @param onFinished The object to call back on when the send has - * completed, or null for no callback. + * completed, or {@code null} for no callback. * @param handler Handler identifying the thread on which the callback - * should happen. If null, the callback will happen from the thread + * should happen. If {@code null}, the callback will happen from the thread * pool of the process. * @param requiredPermission Name of permission that a recipient of the PendingIntent * is required to hold. This is only valid for broadcast intents, and * corresponds to the permission argument in * {@link Context#sendBroadcast(Intent, String) Context.sendOrderedBroadcast(Intent, String)}. - * If null, no permission is required. + * If {@code null}, no permission is required. * * * @throws SendIntentException Throws CanceledIntentException if the IntentSender @@ -198,8 +203,8 @@ public class IntentSender implements Parcelable { public void sendIntent(Context context, int code, Intent intent, OnFinished onFinished, Handler handler, String requiredPermission) throws SendIntentException { - sendIntent(context, code, intent, onFinished, handler, requiredPermission, - SEND_INTENT_DEFAULT_OPTIONS); + sendIntent(context, code, intent, requiredPermission, SEND_INTENT_DEFAULT_OPTIONS, + handler == null ? null : handler::post, onFinished); } /** @@ -207,32 +212,32 @@ public class IntentSender implements Parcelable { * caller to specify information about the Intent to use and be notified * when the send has completed. * - * @param context The Context of the caller. This may be null if - * <var>intent</var> is also null. + * @param context The Context of the caller. This may be {@code null} if + * <var>intent</var> is also {@code null}. * @param code Result code to supply back to the IntentSender's target. * @param intent Additional Intent data. See {@link Intent#fillIn * Intent.fillIn()} for information on how this is applied to the - * original Intent. Use null to not modify the original Intent. + * original Intent. Use {@code null} to not modify the original Intent. * @param onFinished The object to call back on when the send has - * completed, or null for no callback. - * @param handler Handler identifying the thread on which the callback - * should happen. If null, the callback will happen from the thread + * completed, or {@code null} for no callback. + * @param executor Executor identifying the thread on which the callback + * should happen. If {@code null}, the callback will happen from the thread * pool of the process. * @param requiredPermission Name of permission that a recipient of the PendingIntent * is required to hold. This is only valid for broadcast intents, and * corresponds to the permission argument in * {@link Context#sendBroadcast(Intent, String) Context.sendOrderedBroadcast(Intent, String)}. - * If null, no permission is required. + * If {@code null}, no permission is required. * @param options Additional options the caller would like to provide to modify the sending - * behavior. May be built from an {@link ActivityOptions} to apply to an activity start. + * behavior. Typically built from using {@link ActivityOptions} to apply to an activity start. * * @throws SendIntentException Throws CanceledIntentException if the IntentSender * is no longer allowing more intents to be sent through it. - * @hide */ - public void sendIntent(Context context, int code, Intent intent, - OnFinished onFinished, Handler handler, String requiredPermission, - @Nullable Bundle options) + @FlaggedApi(Flags.FLAG_BAL_SEND_INTENT_WITH_OPTIONS) + public void sendIntent(@Nullable Context context, int code, @Nullable Intent intent, + @Nullable String requiredPermission, @Nullable Bundle options, + @Nullable Executor executor, @Nullable OnFinished onFinished) throws SendIntentException { try { String resolvedType = intent != null ? @@ -243,7 +248,7 @@ public class IntentSender implements Parcelable { int res = ActivityManager.getService().sendIntentSender(app, mTarget, mWhitelistToken, code, intent, resolvedType, onFinished != null - ? new FinishedDispatcher(this, onFinished, handler) + ? new FinishedDispatcher(this, onFinished, executor) : null, requiredPermission, options); if (res < 0) { @@ -268,7 +273,7 @@ public class IntentSender implements Parcelable { * sending the Intent. The returned string is supplied by the system, so * that an application can not spoof its package. * - * @return The package name of the PendingIntent, or null if there is + * @return The package name of the PendingIntent, or {@code null} if there is * none associated with it. */ public String getCreatorPackage() { @@ -296,7 +301,7 @@ public class IntentSender implements Parcelable { * {@link android.os.Process#myUserHandle() Process.myUserHandle()} for * more explanation of user handles. * - * @return The user handle of the PendingIntent, or null if there is + * @return The user handle of the PendingIntent, {@code null} if there is * none associated with it. */ public UserHandle getCreatorUserHandle() { @@ -355,11 +360,11 @@ public class IntentSender implements Parcelable { }; /** - * Convenience function for writing either a IntentSender or null pointer to + * Convenience function for writing either a IntentSender or {@code null} pointer to * a Parcel. You must use this with {@link #readIntentSenderOrNullFromParcel} * for later reading it. * - * @param sender The IntentSender to write, or null. + * @param sender The IntentSender to write, or {@code null}. * @param out Where to write the IntentSender. */ public static void writeIntentSenderOrNullToParcel(IntentSender sender, @@ -369,13 +374,13 @@ public class IntentSender implements Parcelable { } /** - * Convenience function for reading either a Messenger or null pointer from + * Convenience function for reading either a Messenger or {@code null} pointer from * a Parcel. You must have previously written the Messenger with * {@link #writeIntentSenderOrNullToParcel}. * * @param in The Parcel containing the written Messenger. * - * @return Returns the Messenger read from the Parcel, or null if null had + * @return Returns the Messenger read from the Parcel, or @code null} if @code null} had * been written. */ public static IntentSender readIntentSenderOrNullFromParcel(Parcel in) { diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 57ffed47bbe3..6952a09f2d90 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -941,6 +941,8 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { CONFIG_COLOR_MODE, CONFIG_FONT_SCALE, CONFIG_GRAMMATICAL_GENDER, + CONFIG_ASSETS_PATHS, + CONFIG_RESOURCES_UNUSED, }) @Retention(RetentionPolicy.SOURCE) public @interface Config {} @@ -1060,8 +1062,8 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { * can itself handle asset path changes. Set from the {@link android.R.attr#configChanges} * attribute. This is not a core resource configuration, but a higher-level value, so its * constant starts at the high bits. - * @hide We do not want apps handling this yet, but we do need some kind of bit for diffs. */ + @FlaggedApi(android.content.res.Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES) public static final int CONFIG_ASSETS_PATHS = 0x80000000; /** * Bit in {@link #configChanges} that indicates that the activity @@ -1088,6 +1090,30 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { */ public static final int CONFIG_FONT_WEIGHT_ADJUSTMENT = 0x10000000; + /** + * <p>This is probably not the constant you want, the resources compiler supports a less + * dangerous version of it, 'allKnown', that only suppresses all currently existing + * configuration change restarts depending on your target SDK rather than whatever the latest + * SDK supports, allowing the application to work with resources on future Platform versions. + * + * <p>Bit in {@link #configChanges} that indicates that the activity doesn't use Android + * Resources at all and doesn't need to be restarted on any configuration changes. This bit + * disables all restarts for configuration dimensions available in the current target SDK as + * well as dimensions introduced in future SDKs. Use it only if the activity doesn't need + * anything from its resources, and doesn't depend on any libraries that may provide resources + * and need to respond to configuration changes. When set, + * {@link Activity#onConfigurationChanged(Configuration)} will be called instead of a restart, + * and it’s up to the implementation to ensure that no stale resource values remain loaded + * anywhere in the code. + * + * <p>This overrides all other bits, and this is recommended to be used individually. + * + * <p>This is not a core resource configuration, but a higher-level value, so its constant + * starts at the high bits. + */ + @FlaggedApi(android.content.res.Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES) + public static final int CONFIG_RESOURCES_UNUSED = 0x8000000; + /** @hide * Unfortunately the constants for config changes in native code are * different from ActivityInfo. :( Here are the values we should use for the @@ -1657,7 +1683,8 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { * {@link #CONFIG_KEYBOARD}, {@link #CONFIG_NAVIGATION}, * {@link #CONFIG_ORIENTATION}, {@link #CONFIG_SCREEN_LAYOUT}, * {@link #CONFIG_DENSITY}, {@link #CONFIG_LAYOUT_DIRECTION}, - * {@link #CONFIG_COLOR_MODE}, and {link #CONFIG_GRAMMATICAL_GENDER}. + * {@link #CONFIG_COLOR_MODE}, {@link #CONFIG_GRAMMATICAL_GENDER}, + * {@link #CONFIG_ASSETS_PATHS}, and {@link #CONFIG_RESOURCES_UNUSED}. * Set from the {@link android.R.attr#configChanges} attribute. */ public int configChanges; diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index c7d94c66c81e..26ee4e821ede 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -161,6 +161,16 @@ flag { } flag { + name: "cache_profile_parent" + namespace: "multiuser" + description: "Cache getProfileParent to avoid unnecessary binder calls" + bug: "350417399" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "cache_quiet_mode_state" namespace: "multiuser" description: "Optimise quiet mode state retrieval" diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig index a475cc85e921..a5f8199c9a07 100644 --- a/core/java/android/content/res/flags.aconfig +++ b/core/java/android/content/res/flags.aconfig @@ -56,3 +56,13 @@ flag { # This flag is read in ResourcesImpl at boot time. is_fixed_read_only: true } + +flag { + name: "handle_all_config_changes" + is_exported: true + namespace: "resource_manager" + description: "Feature flag for allowing activities to handle all kinds of configuration changes" + bug: "180625460" + # This flag is read at boot time. + is_fixed_read_only: true +} diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 6201359ca53e..6a7ee7fcb26a 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -56,6 +56,8 @@ import android.hardware.camera2.utils.ConcurrentCameraIdCombination; import android.hardware.camera2.utils.ExceptionUtils; import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; +import android.hardware.devicestate.feature.flags.FeatureFlags; +import android.hardware.devicestate.feature.flags.FeatureFlagsImpl; import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Handler; @@ -247,14 +249,22 @@ public final class CameraManager { private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners = new ArrayList<>(); private boolean mFoldedDeviceState; + private final FeatureFlags mDeviceStateManagerFlags; public FoldStateListener(Context context) { mFoldedDeviceStates = context.getResources().getIntArray( com.android.internal.R.array.config_foldedDeviceStates); + mDeviceStateManagerFlags = new FeatureFlagsImpl(); } - private synchronized void handleStateChange(int state) { - boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state); + private synchronized void handleStateChange(DeviceState state) { + final boolean folded; + if (mDeviceStateManagerFlags.deviceStatePropertyMigration()) { + folded = state.hasProperty( + DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY); + } else { + folded = ArrayUtils.contains(mFoldedDeviceStates, state.getIdentifier()); + } mFoldedDeviceState = folded; Iterator<WeakReference<DeviceStateListener>> it = mDeviceStateListeners.iterator(); @@ -276,10 +286,8 @@ public final class CameraManager { @SuppressWarnings("FlaggedApi") @Override - public void onDeviceStateChanged(DeviceState state) { - // Suppressing the FlaggedAPI warning as this specific API isn't new, just moved to - // system API which requires it to be flagged. - handleStateChange(state.getIdentifier()); + public void onDeviceStateChanged(@NonNull DeviceState state) { + handleStateChange(state); } } diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java index febc24c1465a..5b511cc94ac0 100644 --- a/core/java/android/hardware/devicestate/DeviceStateManager.java +++ b/core/java/android/hardware/devicestate/DeviceStateManager.java @@ -67,6 +67,14 @@ public final class DeviceStateManager { public static final int MAXIMUM_DEVICE_STATE_IDENTIFIER = 10000; /** + * {@link DeviceState} to represent an invalid device state. + * @hide + */ + public static final DeviceState INVALID_DEVICE_STATE = new DeviceState( + new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, + "INVALID").build()); + + /** * Intent needed to launch the rear display overlay activity from SysUI * * @hide diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 4894fb1ec452..0321e1dfee78 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -395,7 +395,7 @@ public final class DisplayManager { * the display is removed. * * Public virtual displays without this flag will move their content to main display - * stack once they're removed. Private vistual displays will always destroy their + * stack once they're removed. Private virtual displays will always destroy their * content on removal even without this flag. * * @see #createVirtualDisplay diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 91caedc0b9a9..811a834dff47 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -686,8 +686,12 @@ public abstract class DisplayManagerInternal { public RefreshRateRange range; public RefreshRateLimitation(@RefreshRateLimitType int type, float min, float max) { + this(type, new RefreshRateRange(min, max)); + } + + public RefreshRateLimitation(@RefreshRateLimitType int type, RefreshRateRange range) { this.type = type; - range = new RefreshRateRange(min, max); + this.range = range; } @Override diff --git a/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java b/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java index 3be911abe732..8c98abd6dbfe 100644 --- a/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java +++ b/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java @@ -107,7 +107,8 @@ public final class VirtualRotaryEncoderScrollEvent implements Parcelable { } /** - * Sets the scroll amount, normalized from -1.0 to 1.0, inclusive. + * Sets the scroll amount, normalized from -1.0 to 1.0, inclusive. By default, the scroll + * amount is 0, which results in no scroll. * <p> * Positive values indicate scrolling forward (e.g. down in a vertical list); negative * values, backward. diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 16d9ef270f75..0cd280002dbf 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -68,3 +68,10 @@ flag { description: "Controls if the mouse keys accessibility feature for physical keyboard is available to the user" bug: "341799888" } + +flag { + namespace: "input_native" + name: "touchpad_visualizer" + description: "Enables a developer overlay that displays raw touchpad input data and gesture recognition status in real-time." + bug: "286551975" +} diff --git a/core/java/android/inputmethodservice/ImsConfigurationTracker.java b/core/java/android/inputmethodservice/ImsConfigurationTracker.java index 30ef0a2a6f7f..17a5ffcb1081 100644 --- a/core/java/android/inputmethodservice/ImsConfigurationTracker.java +++ b/core/java/android/inputmethodservice/ImsConfigurationTracker.java @@ -16,6 +16,8 @@ package android.inputmethodservice; +import static android.content.pm.ActivityInfo.CONFIG_RESOURCES_UNUSED; + import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; @@ -88,12 +90,16 @@ public final class ImsConfigurationTracker { if (!mInitialized) { return; } + // Don't restart InputMethodService that claims it does not use resources at all. + boolean neverReset = android.content.res.Flags.handleAllConfigChanges() + && (mHandledConfigChanges & CONFIG_RESOURCES_UNUSED) != 0; + final int diff = mLastKnownConfig != null ? mLastKnownConfig.diffPublicOnly(newConfig) : CONFIG_CHANGED; // If the new config is the same as the config this Service is already running with, // then don't bother calling resetStateForNewConfiguration. final int unhandledDiff = (diff & ~mHandledConfigChanges); - if (unhandledDiff != 0) { + if (unhandledDiff != 0 && !neverReset) { resetStateForNewConfigurationRunner.run(); } if (diff != 0) { diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarConstants.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarConstants.java index 4bb66ed26cbc..93c54395f972 100644 --- a/core/java/android/inputmethodservice/navigationbar/NavigationBarConstants.java +++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarConstants.java @@ -17,7 +17,6 @@ package android.inputmethodservice.navigationbar; import android.annotation.ColorInt; -import android.graphics.Color; final class NavigationBarConstants { private NavigationBarConstants() { @@ -28,13 +27,13 @@ final class NavigationBarConstants { // TODO(b/215443343): Handle this in the drawable then remove this constant. static final float NAVBAR_BACK_BUTTON_IME_OFFSET = 2.0f; - // Copied from "white" at packages/SettingsLib/res/values/colors.xml + // Copied from "light_mode_icon_color_single_tone" at packages/SettingsLib/res/values/colors.xml @ColorInt - static final int WHITE = Color.WHITE; + static final int LIGHT_MODE_ICON_COLOR_SINGLE_TONE = 0xffffffff; - // Copied from "black" at packages/SettingsLib/res/values/colors.xml + // Copied from "dark_mode_icon_color_single_tone" at packages/SettingsLib/res/values/colors.xml @ColorInt - static final int BLACK = Color.BLACK; + static final int DARK_MODE_ICON_COLOR_SINGLE_TONE = 0x99000000; // Copied from "navigation_bar_deadzone_hold" static final int NAVIGATION_BAR_DEADZONE_HOLD = 333; diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java index b522e9bdca45..e28f34528f42 100644 --- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java +++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java @@ -16,8 +16,8 @@ package android.inputmethodservice.navigationbar; -import static android.inputmethodservice.navigationbar.NavigationBarConstants.BLACK; -import static android.inputmethodservice.navigationbar.NavigationBarConstants.WHITE; +import static android.inputmethodservice.navigationbar.NavigationBarConstants.DARK_MODE_ICON_COLOR_SINGLE_TONE; +import static android.inputmethodservice.navigationbar.NavigationBarConstants.LIGHT_MODE_ICON_COLOR_SINGLE_TONE; import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVBAR_BACK_BUTTON_IME_OFFSET; import static android.inputmethodservice.navigationbar.NavigationBarUtils.dpToPx; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; @@ -83,8 +83,8 @@ public final class NavigationBarView extends FrameLayout { super(context, attrs); mLightContext = context; - mLightIconColor = WHITE; - mDarkIconColor = BLACK; + mLightIconColor = LIGHT_MODE_ICON_COLOR_SINGLE_TONE; + mDarkIconColor = DARK_MODE_ICON_COLOR_SINGLE_TONE; mConfiguration = new Configuration(); mTmpLastConfiguration = new Configuration(); diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index f30a9f5411ee..3aa42c6bb594 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -3927,7 +3927,12 @@ public class UserManager { android.Manifest.permission.QUERY_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true) public @NonNull UserProperties getUserProperties(@NonNull UserHandle userHandle) { - return mUserPropertiesCache.query(userHandle.getIdentifier()); + final int userId = userHandle.getIdentifier(); + // Avoid calling into system server for invalid user ids. + if (android.multiuser.Flags.fixGetUserPropertyCache() && userId < 0) { + throw new IllegalArgumentException("Cannot access properties for user " + userId); + } + return mUserPropertiesCache.query(userId); } /** @@ -5663,6 +5668,33 @@ public class UserManager { } } + private static final String CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY = + PropertyInvalidatedCache.createPropertyName( + PropertyInvalidatedCache.MODULE_SYSTEM, "quiet_mode_enabled"); + + private final PropertyInvalidatedCache<Integer, Boolean> mQuietModeEnabledCache = + new PropertyInvalidatedCache<Integer, Boolean>( + 32, CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY) { + @Override + public Boolean recompute(Integer query) { + try { + return mService.isQuietModeEnabled(query); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + @Override + public boolean bypass(Integer query) { + return query < 0; + } + }; + + + /** @hide */ + public static final void invalidateQuietModeEnabledCache() { + PropertyInvalidatedCache.invalidateCache(CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY); + } + /** * Returns whether the given profile is in quiet mode or not. * @@ -5670,6 +5702,13 @@ public class UserManager { * @return true if the profile is in quiet mode, false otherwise. */ public boolean isQuietModeEnabled(UserHandle userHandle) { + if (android.multiuser.Flags.cacheQuietModeState()){ + final int userId = userHandle.getIdentifier(); + if (userId < 0) { + return false; + } + return mQuietModeEnabledCache.query(userId); + } try { return mService.isQuietModeEnabled(userHandle.getIdentifier()); } catch (RemoteException re) { diff --git a/core/java/android/tracing/flags.aconfig b/core/java/android/tracing/flags.aconfig index be60c2504f38..04dba462bc1f 100644 --- a/core/java/android/tracing/flags.aconfig +++ b/core/java/android/tracing/flags.aconfig @@ -46,3 +46,11 @@ flag { is_fixed_read_only: true bug: "323165543" } + +flag { + name: "client_side_proto_logging" + namespace: "windowing_tools" + description: "Add support for client side protologging" + is_fixed_read_only: true + bug: "352538294" +} diff --git a/core/java/android/view/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java index fe98faba98c3..075571cb2890 100644 --- a/core/java/android/view/InsetsFrameProvider.java +++ b/core/java/android/view/InsetsFrameProvider.java @@ -162,6 +162,12 @@ public class InsetsFrameProvider implements Parcelable { return mSource; } + /** Set the flags of this provider. */ + public InsetsFrameProvider setFlags(@Flags int flags) { + mFlags = flags; + return this; + } + public InsetsFrameProvider setFlags(@Flags int flags, @Flags int mask) { mFlags = (mFlags & ~mask) | (flags & mask); return this; diff --git a/core/java/android/view/SurfaceControlRegistry.java b/core/java/android/view/SurfaceControlRegistry.java index 117b200d054e..a806bd226c36 100644 --- a/core/java/android/view/SurfaceControlRegistry.java +++ b/core/java/android/view/SurfaceControlRegistry.java @@ -295,16 +295,7 @@ public class SurfaceControlRegistry { } sCallStackDebuggingInitialized = true; - sCallStackDebuggingMatchCall = - SystemProperties.get("persist.wm.debug.sc.tx.log_match_call", null) - .toLowerCase(); - sCallStackDebuggingMatchName = - SystemProperties.get("persist.wm.debug.sc.tx.log_match_name", null) - .toLowerCase(); - sLogAllTxCallsOnApply = sCallStackDebuggingMatchCall.contains("apply"); - // Only enable stack debugging if any of the match filters are set - sCallStackDebuggingEnabled = !sCallStackDebuggingMatchCall.isEmpty() - || !sCallStackDebuggingMatchName.isEmpty(); + updateCallStackDebuggingParams(); if (sCallStackDebuggingEnabled) { Log.d(TAG, "Enabling transaction call stack debugging:" + " matchCall=" + sCallStackDebuggingMatchCall @@ -325,6 +316,11 @@ public class SurfaceControlRegistry { final void checkCallStackDebugging(@NonNull String call, @Nullable SurfaceControl.Transaction tx, @Nullable SurfaceControl sc, @Nullable String details) { + + if (sCallStackDebuggingInitialized && sCallStackDebuggingEnabled) { + updateCallStackDebuggingParams(); + } + if (!sCallStackDebuggingEnabled) { return; } @@ -356,6 +352,22 @@ public class SurfaceControlRegistry { } /** + * Updates the call stack debugging params from the system properties. + */ + private static void updateCallStackDebuggingParams() { + sCallStackDebuggingMatchCall = + SystemProperties.get("persist.wm.debug.sc.tx.log_match_call", null) + .toLowerCase(); + sCallStackDebuggingMatchName = + SystemProperties.get("persist.wm.debug.sc.tx.log_match_name", null) + .toLowerCase(); + sLogAllTxCallsOnApply = sCallStackDebuggingMatchCall.contains("apply"); + // Only enable stack debugging if any of the match filters are set + sCallStackDebuggingEnabled = !sCallStackDebuggingMatchCall.isEmpty() + || !sCallStackDebuggingMatchName.isEmpty(); + } + + /** * Tests whether the given surface control name/method call matches the filters set for the * call stack debugging. */ diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index d224217de4b7..e5b17c8d1001 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -128,8 +128,8 @@ import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodCl import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme; -import static com.android.window.flags.Flags.activityWindowInfoFlag; import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay; +import static com.android.window.flags.Flags.enableCaptionCompatInsetForceConsumption; import static com.android.window.flags.Flags.insetsControlChangedItem; import static com.android.window.flags.Flags.insetsControlSeq; import static com.android.window.flags.Flags.setScPropertiesInClient; @@ -1373,9 +1373,6 @@ public final class ViewRootImpl implements ViewParent, */ public void setActivityConfigCallback(@Nullable ActivityConfigCallback callback) { mActivityConfigCallback = callback; - if (!activityWindowInfoFlag()) { - return; - } if (callback == null) { mPendingActivityWindowInfo = null; mLastReportedActivityWindowInfo = null; @@ -3189,7 +3186,9 @@ public final class ViewRootImpl implements ViewParent, inOutParams.privateFlags &= ~PRIVATE_FLAG_FIT_INSETS_CONTROLLED; } - private void controlInsetsForCompatibility(WindowManager.LayoutParams params) { + /** Updates the {@link InsetsType}s to hide or show based on layout params. */ + @VisibleForTesting + public void controlInsetsForCompatibility(WindowManager.LayoutParams params) { final int sysUiVis = params.systemUiVisibility | params.subtreeSystemUiVisibility; final int flags = params.flags; final boolean matchParent = params.width == MATCH_PARENT && params.height == MATCH_PARENT; @@ -3200,6 +3199,9 @@ public final class ViewRootImpl implements ViewParent, || ((flags & FLAG_FULLSCREEN) != 0 && matchParent && nonAttachedAppWindow); final boolean navWasHiddenByFlags = (mTypesHiddenByFlags & Type.navigationBars()) != 0; final boolean navIsHiddenByFlags = (sysUiVis & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0; + final boolean captionWasHiddenByFlags = (mTypesHiddenByFlags & Type.captionBar()) != 0; + final boolean captionIsHiddenByFlags = (sysUiVis & SYSTEM_UI_FLAG_FULLSCREEN) != 0 + || ((flags & FLAG_FULLSCREEN) != 0 && matchParent && nonAttachedAppWindow); @InsetsType int typesToHide = 0; @InsetsType int typesToShow = 0; @@ -3213,6 +3215,13 @@ public final class ViewRootImpl implements ViewParent, } else if (!navIsHiddenByFlags && navWasHiddenByFlags) { typesToShow |= Type.navigationBars(); } + if (captionIsHiddenByFlags && !captionWasHiddenByFlags + && enableCaptionCompatInsetForceConsumption()) { + typesToHide |= Type.captionBar(); + } else if (!captionIsHiddenByFlags && captionWasHiddenByFlags + && enableCaptionCompatInsetForceConsumption()) { + typesToShow |= Type.captionBar(); + } if (typesToHide != 0) { getInsetsController().hide(typesToHide); } @@ -9347,7 +9356,7 @@ public final class ViewRootImpl implements ViewParent, onClientWindowFramesChanged(mTmpFrames); - if (activityWindowInfoFlag() && mPendingActivityWindowInfo != null) { + if (mPendingActivityWindowInfo != null) { final ActivityWindowInfo outInfo = mRelayoutResult.activityWindowInfo; if (outInfo != null) { mPendingActivityWindowInfo.set(outInfo); diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index 11ee286652a1..098f65575928 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -92,7 +92,7 @@ public final class InputMethodInfo implements Parcelable { * * @see #createImeLanguageSettingsActivityIntent() */ - @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP) + @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) public static final String ACTION_IME_LANGUAGE_SETTINGS = "android.view.inputmethod.action.IME_LANGUAGE_SETTINGS"; @@ -298,7 +298,7 @@ public final class InputMethodInfo implements Parcelable { com.android.internal.R.styleable.InputMethod); settingsActivityComponent = sa.getString( com.android.internal.R.styleable.InputMethod_settingsActivity); - if (Flags.imeSwitcherRevamp()) { + if (Flags.imeSwitcherRevampApi()) { languageSettingsActivityComponent = sa.getString( com.android.internal.R.styleable.InputMethod_languageSettingsActivity); } @@ -888,7 +888,7 @@ public final class InputMethodInfo implements Parcelable { * * @attr ref R.styleable#InputMethod_languageSettingsActivity */ - @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP) + @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) @Nullable public Intent createImeLanguageSettingsActivityIntent() { if (TextUtils.isEmpty(mLanguageSettingsActivityName)) { diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index 56e5bcf79933..e294ee2d3a91 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -82,6 +82,15 @@ flag { } flag { + name: "ime_switcher_revamp_api" + is_exported: true + namespace: "input_method" + description: "Feature flag for APIs for revamping the Input Method Switcher menu" + bug: "311791923" + is_fixed_read_only: true +} + +flag { name: "initiation_without_input_connection" namespace: "input_method" description: "Feature flag for initiating handwriting without InputConnection" diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index 10344792eee2..c53a0e158dea 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -36,6 +36,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.Trace; +import android.os.UserHandle; import android.text.TextUtils; import android.util.AndroidRuntimeException; import android.util.ArraySet; @@ -339,10 +340,10 @@ public final class WebViewFactory { if (sProviderInstance != null) return sProviderInstance; sTimestamps.mWebViewLoadStart = SystemClock.uptimeMillis(); - final int uid = android.os.Process.myUid(); - if (uid == android.os.Process.ROOT_UID || uid == android.os.Process.SYSTEM_UID - || uid == android.os.Process.PHONE_UID || uid == android.os.Process.NFC_UID - || uid == android.os.Process.BLUETOOTH_UID) { + final int appId = UserHandle.getAppId(android.os.Process.myUid()); + if (appId == android.os.Process.ROOT_UID || appId == android.os.Process.SYSTEM_UID + || appId == android.os.Process.PHONE_UID || appId == android.os.Process.NFC_UID + || appId == android.os.Process.BLUETOOTH_UID) { throw new UnsupportedOperationException( "For security reasons, WebView is not allowed in privileged processes"); } diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java index d28500c0a1ea..12d4ab8bc963 100644 --- a/core/java/android/window/BackProgressAnimator.java +++ b/core/java/android/window/BackProgressAnimator.java @@ -16,12 +16,15 @@ package android.window; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; + import android.annotation.NonNull; import android.annotation.Nullable; import android.util.FloatProperty; import android.util.TimeUtils; import android.view.Choreographer; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.dynamicanimation.animation.DynamicAnimation; import com.android.internal.dynamicanimation.animation.FlingAnimation; import com.android.internal.dynamicanimation.animation.FloatValueHolder; @@ -210,7 +213,8 @@ public class BackProgressAnimator implements DynamicAnimation.OnAnimationUpdateL } /** Returns true if the back animation is in progress. */ - boolean isBackAnimationInProgress() { + @VisibleForTesting(visibility = PACKAGE) + public boolean isBackAnimationInProgress() { return mBackAnimationInProgress; } diff --git a/core/java/android/window/ITaskFragmentOrganizerController.aidl b/core/java/android/window/ITaskFragmentOrganizerController.aidl index 4706dfd1a76c..2c64b8ed456d 100644 --- a/core/java/android/window/ITaskFragmentOrganizerController.aidl +++ b/core/java/android/window/ITaskFragmentOrganizerController.aidl @@ -39,19 +39,6 @@ interface ITaskFragmentOrganizerController { void unregisterOrganizer(in ITaskFragmentOrganizer organizer); /** - * Registers remote animations per transition type for the organizer. It will override the - * animations if the transition only contains windows that belong to the organized - * TaskFragments in the given Task. - */ - void registerRemoteAnimations(in ITaskFragmentOrganizer organizer, - in RemoteAnimationDefinition definition); - - /** - * Unregisters remote animations per transition type for the organizer. - */ - void unregisterRemoteAnimations(in ITaskFragmentOrganizer organizer); - - /** * Checks if an activity organized by a {@link android.window.TaskFragmentOrganizer} and * only occupies a portion of Task bounds. */ diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java index 461eab61e393..15f1258ccd69 100644 --- a/core/java/android/window/TaskFragmentOrganizer.java +++ b/core/java/android/window/TaskFragmentOrganizer.java @@ -32,7 +32,6 @@ import android.annotation.TestApi; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; -import android.view.RemoteAnimationDefinition; import android.view.WindowManager; import com.android.window.flags.Flags; @@ -205,34 +204,6 @@ public class TaskFragmentOrganizer extends WindowOrganizer { } /** - * Registers remote animations per transition type for the organizer. It will override the - * animations if the transition only contains windows that belong to the organized - * TaskFragments, and at least one of the transition window is embedded (not filling the Task). - * @hide - */ - @CallSuper - public void registerRemoteAnimations(@NonNull RemoteAnimationDefinition definition) { - try { - getController().registerRemoteAnimations(mInterface, definition); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** - * Unregisters remote animations per transition type for the organizer. - * @hide - */ - @CallSuper - public void unregisterRemoteAnimations() { - try { - getController().unregisterRemoteAnimations(mInterface); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** * Notifies the server that the organizer has finished handling the given transaction. The * server should apply the given {@link WindowContainerTransaction} for the necessary changes. * @@ -357,6 +328,7 @@ public class TaskFragmentOrganizer extends WindowOrganizer { * only occupies a portion of Task bounds. * @hide */ + // TODO(b/287582673): cleanup public boolean isActivityEmbedded(@NonNull IBinder activityToken) { try { return getController().isActivityEmbedded(activityToken); diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 8ded608b71e1..1083f64513b1 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -205,7 +205,7 @@ public final class TransitionInfo implements Parcelable { FLAG_SYNC, FLAG_CONFIG_AT_END, FLAG_FIRST_CUSTOM - }) + }, flag = true) public @interface ChangeFlags {} private final @TransitionType int mType; diff --git a/core/java/android/window/TransitionRequestInfo.java b/core/java/android/window/TransitionRequestInfo.java index cc225767bd49..253337b553da 100644 --- a/core/java/android/window/TransitionRequestInfo.java +++ b/core/java/android/window/TransitionRequestInfo.java @@ -471,7 +471,7 @@ public final class TransitionRequestInfo implements Parcelable { "pipTask = " + mPipTask + ", " + "remoteTransition = " + mRemoteTransition + ", " + "displayChange = " + mDisplayChange + ", " + - "flags = " + mFlags + ", " + + "flags = " + Integer.toHexString(mFlags) + ", " + "debugId = " + mDebugId + " }"; } diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index cd033caf4871..e9d77f8aaf80 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -42,6 +42,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; import android.view.InsetsFrameProvider; +import android.view.InsetsSource; import android.view.SurfaceControl; import android.view.WindowInsets.Type.InsetsType; @@ -714,14 +715,16 @@ public final class WindowContainerTransaction implements Parcelable { @NonNull public WindowContainerTransaction addInsetsSource( @NonNull WindowContainerToken receiver, - IBinder owner, int index, @InsetsType int type, Rect frame, Rect[] boundingRects) { + IBinder owner, int index, @InsetsType int type, Rect frame, Rect[] boundingRects, + @InsetsSource.Flags int flags) { final HierarchyOp hierarchyOp = new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER) .setContainer(receiver.asBinder()) .setInsetsFrameProvider(new InsetsFrameProvider(owner, index, type) .setSource(InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE) .setArbitraryRectangle(frame) - .setBoundingRects(boundingRects)) + .setBoundingRects(boundingRects) + .setFlags(flags)) .setInsetsFrameOwner(owner) .build(); mHierarchyOps.add(hierarchyOp); diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 9b87e2351e3f..7bbc3dbb29db 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -244,6 +244,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { // We should call onBackCancelled() when an active callback is removed from // dispatcher. sendCancelledIfInProgress(callback); + mHandler.post(mProgressAnimator::reset); setTopOnBackInvokedCallback(getTopCallback()); } } diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index f7329298f848..1362f7bf9619 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -168,4 +168,11 @@ flag { namespace: "lse_desktop_experience" description: "Whether to enable back navigation treatment in desktop windowing." bug: "350421096" -}
\ No newline at end of file +} + +flag { + name: "enable_desktop_windowing_app_handle_education" + namespace: "lse_desktop_experience" + description: "Enables desktop windowing app handle education" + bug: "348208342" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 92db37eea4ad..5397e91bd249 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -206,3 +206,22 @@ flag { } } +flag { + name: "enforce_shell_thread_model" + namespace: "windowing_frontend" + description: "Crash the shell process if someone calls in from the wrong thread" + bug: "351189446" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} +flag { + name: "custom_animations_behind_translucent" + namespace: "windowing_frontend" + description: "A change can use its own layer parameters to animate behind a translucent activity" + bug: "327332488" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/app/SuspendedAppActivity.java b/core/java/com/android/internal/app/SuspendedAppActivity.java index 7a8a47e5f9fc..9d5704141c93 100644 --- a/core/java/com/android/internal/app/SuspendedAppActivity.java +++ b/core/java/com/android/internal/app/SuspendedAppActivity.java @@ -342,8 +342,8 @@ public class SuspendedAppActivity extends AlertActivity .MODE_BACKGROUND_ACTIVITY_START_ALLOWED) .toBundle(); try { - mOnUnsuspend.sendIntent(this, 0, null, null, null, null, - activityOptions); + mOnUnsuspend.sendIntent(this, 0, null, null, activityOptions, + null, null); } catch (IntentSender.SendIntentException e) { Slog.e(TAG, "Error while starting intent " + mOnUnsuspend, e); } diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 63ff598bae61..87e22ed42cbd 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -109,6 +109,7 @@ import com.android.internal.view.menu.MenuHelper; import com.android.internal.widget.ActionBarContextView; import com.android.internal.widget.BackgroundFallback; import com.android.internal.widget.floatingtoolbar.FloatingToolbar; +import com.android.window.flags.Flags; import java.util.List; import java.util.function.Consumer; @@ -1193,7 +1194,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // If we should always consume system bars, only consume that if the app wanted to go to // fullscreen, as otherwise we can expect the app to handle it. boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0 - || (attrs.flags & FLAG_FULLSCREEN) != 0 + || (attrs.flags & FLAG_FULLSCREEN) != 0; + final boolean hideStatusBar = fullscreen || (requestedVisibleTypes & WindowInsets.Type.statusBars()) == 0; boolean consumingStatusBar = ((sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0 @@ -1203,9 +1205,20 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind && mForceWindowDrawsBarBackgrounds && mLastTopInset != 0) || ((mLastForceConsumingTypes & WindowInsets.Type.statusBars()) != 0 - && fullscreen); + && hideStatusBar); - int consumedTop = consumingStatusBar ? mLastTopInset : 0; + final boolean hideCaptionBar = fullscreen + || (requestedVisibleTypes & WindowInsets.Type.captionBar()) == 0; + final boolean consumingCaptionBar = + ((mLastForceConsumingTypes & WindowInsets.Type.captionBar()) != 0 + && hideCaptionBar); + + final int consumedTop; + if (Flags.enableCaptionCompatInsetForceConsumption()) { + consumedTop = (consumingStatusBar || consumingCaptionBar) ? mLastTopInset : 0; + } else { + consumedTop = consumingStatusBar ? mLastTopInset : 0; + } int consumedRight = consumingNavBar ? mLastRightInset : 0; int consumedBottom = consumingNavBar ? mLastBottomInset : 0; int consumedLeft = consumingNavBar ? mLastLeftInset : 0; diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 6abba8bed78c..32c4830c674d 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -98,14 +98,15 @@ public class PerfettoProtoLogImpl implements IProtoLog { this::onTracingFlush, this::onTracingInstanceStop ); + @Nullable private final ProtoLogViewerConfigReader mViewerConfigReader; private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>(); private final Runnable mCacheUpdater; private final int[] mDefaultLogLevelCounts = new int[LogLevel.values().length]; - private final Map<IProtoLogGroup, int[]> mLogLevelCounts = new ArrayMap<>(); - private final Map<IProtoLogGroup, Integer> mCollectStackTraceGroupCounts = new ArrayMap<>(); + private final Map<String, int[]> mLogLevelCounts = new ArrayMap<>(); + private final Map<String, Integer> mCollectStackTraceGroupCounts = new ArrayMap<>(); private final Lock mBackgroundServiceLock = new ReentrantLock(); private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor(); @@ -126,7 +127,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { } public PerfettoProtoLogImpl( - ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, + @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, Runnable cacheUpdater ) { this(viewerConfigInputStreamProvider, @@ -136,8 +137,8 @@ public class PerfettoProtoLogImpl implements IProtoLog { @VisibleForTesting public PerfettoProtoLogImpl( - ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, - ProtoLogViewerConfigReader viewerConfigReader, + @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, + @Nullable ProtoLogViewerConfigReader viewerConfigReader, Runnable cacheUpdater ) { Producer.init(InitArguments.DEFAULTS); @@ -209,7 +210,9 @@ public class PerfettoProtoLogImpl implements IProtoLog { * @return status code */ public int startLoggingToLogcat(String[] groups, ILogger logger) { - mViewerConfigReader.loadViewerConfig(logger); + if (mViewerConfigReader != null) { + mViewerConfigReader.loadViewerConfig(logger); + } return setTextLogging(true, logger, groups); } @@ -220,13 +223,15 @@ public class PerfettoProtoLogImpl implements IProtoLog { * @return status code */ public int stopLoggingToLogcat(String[] groups, ILogger logger) { - mViewerConfigReader.unloadViewerConfig(); + if (mViewerConfigReader != null) { + mViewerConfigReader.unloadViewerConfig(); + } return setTextLogging(false, logger, groups); } @Override public boolean isEnabled(IProtoLogGroup group, LogLevel level) { - final int[] groupLevelCount = mLogLevelCounts.get(group); + final int[] groupLevelCount = mLogLevelCounts.get(group.name()); return (groupLevelCount == null && mDefaultLogLevelCounts[level.ordinal()] > 0) || (groupLevelCount != null && groupLevelCount[level.ordinal()] > 0) || group.isLogToLogcat(); @@ -262,7 +267,9 @@ public class PerfettoProtoLogImpl implements IProtoLog { return -1; } case "enable-text" -> { - mViewerConfigReader.loadViewerConfig(logger); + if (mViewerConfigReader != null) { + mViewerConfigReader.loadViewerConfig(logger); + } return setTextLogging(true, logger, groups); } case "disable-text" -> { @@ -279,7 +286,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { if (isProtoEnabled()) { long tsNanos = SystemClock.elapsedRealtimeNanos(); final String stacktrace; - if (mCollectStackTraceGroupCounts.getOrDefault(group, 0) > 0) { + if (mCollectStackTraceGroupCounts.getOrDefault(group.name(), 0) > 0) { stacktrace = collectStackTrace(); } else { stacktrace = null; @@ -420,7 +427,12 @@ public class PerfettoProtoLogImpl implements IProtoLog { private void logToLogcat(String tag, LogLevel level, Message message, @Nullable Object[] args) { - String messageString = message.getMessage(mViewerConfigReader); + String messageString; + if (mViewerConfigReader == null) { + messageString = message.getMessage(); + } else { + messageString = message.getMessage(mViewerConfigReader); + } if (messageString == null) { StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE"); @@ -739,15 +751,8 @@ public class PerfettoProtoLogImpl implements IProtoLog { final Set<String> overriddenGroupTags = config.getGroupTagsWithOverriddenConfigs(); for (String overriddenGroupTag : overriddenGroupTags) { - IProtoLogGroup group = mLogGroups.get(overriddenGroupTag); - - if (group == null) { - throw new IllegalArgumentException("Trying to set config for \"" - + overriddenGroupTag + "\" that isn't registered"); - } - - mLogLevelCounts.putIfAbsent(group, new int[LogLevel.values().length]); - final int[] logLevelsCountsForGroup = mLogLevelCounts.get(group); + mLogLevelCounts.putIfAbsent(overriddenGroupTag, new int[LogLevel.values().length]); + final int[] logLevelsCountsForGroup = mLogLevelCounts.get(overriddenGroupTag); final LogLevel logFromLevel = config.getConfigFor(overriddenGroupTag).logFrom; for (int i = logFromLevel.ordinal(); i < LogLevel.values().length; i++) { @@ -755,13 +760,13 @@ public class PerfettoProtoLogImpl implements IProtoLog { } if (config.getConfigFor(overriddenGroupTag).collectStackTrace) { - mCollectStackTraceGroupCounts.put(group, - mCollectStackTraceGroupCounts.getOrDefault(group, 0) + 1); + mCollectStackTraceGroupCounts.put(overriddenGroupTag, + mCollectStackTraceGroupCounts.getOrDefault(overriddenGroupTag, 0) + 1); } if (config.getConfigFor(overriddenGroupTag).collectStackTrace) { - mCollectStackTraceGroupCounts.put(group, - mCollectStackTraceGroupCounts.getOrDefault(group, 0) + 1); + mCollectStackTraceGroupCounts.put(overriddenGroupTag, + mCollectStackTraceGroupCounts.getOrDefault(overriddenGroupTag, 0) + 1); } } @@ -781,24 +786,22 @@ public class PerfettoProtoLogImpl implements IProtoLog { final Set<String> overriddenGroupTags = config.getGroupTagsWithOverriddenConfigs(); for (String overriddenGroupTag : overriddenGroupTags) { - IProtoLogGroup group = mLogGroups.get(overriddenGroupTag); - - final int[] logLevelsCountsForGroup = mLogLevelCounts.get(group); + final int[] logLevelsCountsForGroup = mLogLevelCounts.get(overriddenGroupTag); final LogLevel logFromLevel = config.getConfigFor(overriddenGroupTag).logFrom; - for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) { + for (int i = logFromLevel.ordinal(); i < LogLevel.values().length; i++) { logLevelsCountsForGroup[i]--; } if (Arrays.stream(logLevelsCountsForGroup).allMatch(it -> it == 0)) { - mLogLevelCounts.remove(group); + mLogLevelCounts.remove(overriddenGroupTag); } if (config.getConfigFor(overriddenGroupTag).collectStackTrace) { - mCollectStackTraceGroupCounts.put(group, - mCollectStackTraceGroupCounts.get(group) - 1); + mCollectStackTraceGroupCounts.put(overriddenGroupTag, + mCollectStackTraceGroupCounts.get(overriddenGroupTag) - 1); - if (mCollectStackTraceGroupCounts.get(group) == 0) { - mCollectStackTraceGroupCounts.remove(group); + if (mCollectStackTraceGroupCounts.get(overriddenGroupTag) == 0) { + mCollectStackTraceGroupCounts.remove(overriddenGroupTag); } } } @@ -836,7 +839,11 @@ public class PerfettoProtoLogImpl implements IProtoLog { return mMessageMask; } - private String getMessage(ProtoLogViewerConfigReader viewerConfigReader) { + private String getMessage() { + return mMessageString; + } + + private String getMessage(@NonNull ProtoLogViewerConfigReader viewerConfigReader) { if (mMessageString != null) { return mMessageString; } diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index eaff7608ce3b..c07fd3838837 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -2718,12 +2718,11 @@ static jint android_media_AudioSystem_getMaxChannelCount(JNIEnv *env, jobject th } static jint android_media_AudioSystem_getMaxSampleRate(JNIEnv *env, jobject thiz) { - // see frameworks/av/services/audiopolicy/common/include/policy.h - return 192000; // SAMPLE_RATE_HZ_MAX (for API) + return SAMPLE_RATE_HZ_MAX; } static jint android_media_AudioSystem_getMinSampleRate(JNIEnv *env, jobject thiz) { - return 4000; // SAMPLE_RATE_HZ_MIN (for API) + return SAMPLE_RATE_HZ_MIN; } static std::vector<uid_t> convertJIntArrayToUidVector(JNIEnv *env, jintArray jArray) { diff --git a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto index b75d545b1305..eddf1d276e4b 100644 --- a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto +++ b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto @@ -47,4 +47,5 @@ message InputMethodManagerServiceProto { optional int32 ime_window_visibility = 22; optional bool show_ime_with_hard_keyboard = 23; optional bool accessibility_requesting_no_soft_keyboard = 24; -}
\ No newline at end of file + optional bool concurrent_multi_user_mode_enabled = 25; +} diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 4892f59bd9fb..0975eda3f9ff 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -4304,7 +4304,7 @@ <attr name="settingsActivity" format="string" /> <!-- Component name of an activity that allows the user to modify on-screen keyboards variants (e.g. different language or layout) for this service. --> - <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") --> + <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") --> <attr name="languageSettingsActivity" format="string"/> <!-- Set to true in all of the configurations for which this input method should be considered an option as the default. --> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index f94c8ab0350e..2e3dbda5e41c 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1052,6 +1052,19 @@ <!-- The font weight adjustment value has changed. Used to reflect the user increasing font weight. --> <flag name="fontWeightAdjustment" value="0x10000000" /> + <!-- The assets paths have changed. For example a runtime overlay is installed and enabled. + Corresponds to {@link android.content.pm.ActivityInfo#CONFIG_ASSETS_PATHS}. --> + <flag name="assetsPaths" value="0x80000000" /> + <!-- This is probably not the flag you want, the resources compiler supports a less + dangerous version of it, 'allKnown', that only suppresses all currently existing + configuration change restarts depending on your target SDK rather than whatever the + latest SDK supports, allowing the application to work with resources on future Platform + versions. + Activity doesn't use Android Resources at all and doesn't need to be restarted on any + configuration changes. This overrides all other flags, and this is recommended to be + used individually. Corresponds to + {@link android.content.pm.ActivityInfo#CONFIG_RESOURCES_UNUSED}. --> + <flag name="resourcesUnused" value="0x8000000" /> </attr> <!-- Indicate that the activity can be launched as the embedded child of another diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index b64334f7f95a..b74b41c666c8 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -114,7 +114,7 @@ <public name="optional"/> <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") --> <public name="adServiceTypes" /> - <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") --> + <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") --> <public name="languageSettingsActivity"/> <!-- @FlaggedApi("android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM") --> <public name="dreamCategory"/> diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java index 36ab0d4b0868..ce85a76f478d 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java @@ -70,7 +70,7 @@ public class InputMethodInfoTest { assertThat(imi.supportsInlineSuggestionsWithTouchExploration(), is(false)); assertThat(imi.supportsStylusHandwriting(), is(false)); assertThat(imi.createStylusHandwritingSettingsActivityIntent(), equalTo(null)); - if (Flags.imeSwitcherRevamp()) { + if (Flags.imeSwitcherRevampApi()) { assertThat(imi.createImeLanguageSettingsActivityIntent(), equalTo(null)); } } diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index f87a9e2b3643..e8a0762f70c0 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -25,8 +25,6 @@ import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; -import static com.android.window.flags.Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG; - import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; @@ -810,7 +808,6 @@ public class ActivityThreadTest { @Test public void testActivityWindowInfoChanged_activityLaunch() { - mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG); ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener( mActivityWindowInfoListener); @@ -825,7 +822,6 @@ public class ActivityThreadTest { @Test public void testActivityWindowInfoChanged_activityRelaunch() { - mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG); ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener( mActivityWindowInfoListener); @@ -866,7 +862,6 @@ public class ActivityThreadTest { @Test public void testActivityWindowInfoChanged_activityConfigurationChanged() { - mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG); ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener( mActivityWindowInfoListener); diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java index 0b270d485b97..d2a444f0d0df 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java @@ -53,8 +53,6 @@ import android.window.WindowTokenClient; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.window.flags.Flags; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -167,8 +165,6 @@ public class ClientTransactionListenerControllerTest { @Test public void testActivityWindowInfoChangedListener() { - mSetFlagsRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); - mController.registerActivityWindowInfoChangedListener(mActivityWindowInfoListener); final ActivityWindowInfo activityWindowInfo = new ActivityWindowInfo(); activityWindowInfo.set(true /* isEmbedded */, new Rect(0, 0, 1000, 2000), diff --git a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java index 064439e9b113..6998c32978c1 100644 --- a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java +++ b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertTrue; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.platform.test.annotations.RequiresFlagsEnabled; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -74,4 +75,33 @@ public class ImsConfigurationTrackerTest { assertFalse("IME shouldn't restart since it handles configChanges", didReset.get()); } + + @Test + @RequiresFlagsEnabled(android.content.res.Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES) + public void testShouldImeRestart_handleResourceUnused() throws Exception { + Configuration config = mContext.getResources().getConfiguration(); + mImsConfigTracker.onInitialize(0 /* handledConfigChanges */); + mImsConfigTracker.onBindInput(mContext.getResources()); + Configuration newConfig = new Configuration(config); + + final AtomicBoolean didReset = new AtomicBoolean(); + Runnable resetStateRunner = () -> didReset.set(true); + + mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner); + assertFalse("IME shouldn't restart if config hasn't changed", + didReset.get()); + + // Screen density changed but IME doesn't handle configChanges + newConfig.densityDpi = 99; + mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner); + assertTrue("IME should restart for unhandled configChanges", + didReset.get()); + + didReset.set(false); + // opt-in IME to handle all configuration changes. + mImsConfigTracker.setHandledConfigChanges(ActivityInfo.CONFIG_RESOURCES_UNUSED); + mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner); + assertFalse("IME shouldn't restart since it handles configChanges", + didReset.get()); + } } diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index d4482f243939..9ae96a0fc9d8 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -348,12 +348,16 @@ public class WindowOnBackInvokedDispatcherTest { waitForIdle(); verify(mCallback1).onBackStarted(any(BackEvent.class)); + assertTrue(mDispatcher.mProgressAnimator.isBackAnimationInProgress()); mDispatcher.unregisterOnBackInvokedCallback(mCallback1); waitForIdle(); verify(mCallback1).onBackCancelled(); verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), isNull()); + // Verify that ProgressAnimator is reset (and thus does not cause further callback event + // dispatching) + assertFalse(mDispatcher.mProgressAnimator.isBackAnimationInProgress()); } @Test diff --git a/core/tests/overlaytests/handle_config_change/Android.bp b/core/tests/overlaytests/handle_config_change/Android.bp new file mode 100644 index 000000000000..2b31d0adab17 --- /dev/null +++ b/core/tests/overlaytests/handle_config_change/Android.bp @@ -0,0 +1,45 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_android_resources", +} + +java_test_host { + name: "HandleConfigChangeHostTests", + srcs: ["src/**/*.java"], + libs: [ + "tradefed", + "compatibility-host-util", + ], + static_libs: [ + "compatibility-host-util-axt", + "flag-junit-host", + "android.content.res.flags-aconfig-java-host", + ], + test_suites: [ + "device-tests", + ], + // All APKs required by the tests + data: [ + ":OverlayResApp", + ], + per_testcase_directory: true, +} diff --git a/core/tests/overlaytests/handle_config_change/AndroidTest.xml b/core/tests/overlaytests/handle_config_change/AndroidTest.xml new file mode 100644 index 000000000000..05ea03686484 --- /dev/null +++ b/core/tests/overlaytests/handle_config_change/AndroidTest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<configuration description="Config for the handle config change test cases"> + <option name="test-tag" value="HandleConfigChangeHostTests" /> + <option name="test-suite-tag" value="apct" /> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <option name="force-skip-system-props" value="true" /> + <option name="set-global-setting" key="verifier_engprod" value="1" /> + <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" /> + <option name="restore-settings" value="true" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="OverlayResApp.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest"> + <option name="class" value="com.android.overlaytest.HandleConfigChangeHostTests" /> + </test> +</configuration> diff --git a/core/tests/overlaytests/handle_config_change/src/com/android/overlaytest/HandleConfigChangeHostTests.java b/core/tests/overlaytests/handle_config_change/src/com/android/overlaytest/HandleConfigChangeHostTests.java new file mode 100644 index 000000000000..e91716ab04b3 --- /dev/null +++ b/core/tests/overlaytests/handle_config_change/src/com/android/overlaytest/HandleConfigChangeHostTests.java @@ -0,0 +1,47 @@ +/* + * 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.overlaytest; + +import android.content.res.Flags; +import android.platform.test.annotations.AppModeFull; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.host.HostFlagsValueProvider; + +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(DeviceJUnit4ClassRunner.class) +@AppModeFull +public class HandleConfigChangeHostTests extends BaseHostJUnit4Test { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + HostFlagsValueProvider.createCheckFlagsRule(this::getDevice); + private static final String DEVICE_TEST_PKG1 = "com.android.overlaytest.overlayresapp"; + private static final String DEVICE_TEST_CLASS = "OverlayResTest"; + + @Test + @RequiresFlagsEnabled(Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES) + public void testOverlayRes() throws Exception { + runDeviceTests(DEVICE_TEST_PKG1, DEVICE_TEST_PKG1 + "." + DEVICE_TEST_CLASS, + "overlayRes_onConfigurationChanged"); + } +} diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/Android.bp b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/Android.bp new file mode 100644 index 000000000000..e0f101229080 --- /dev/null +++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/Android.bp @@ -0,0 +1,43 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test_helper_app { + name: "OverlayResApp", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + certificate: "platform", + + static_libs: [ + "androidx.annotation_annotation", + "androidx.test.rules", + "androidx.test.core", + "compatibility-device-util-axt", + "truth", + ], + libs: [ + "android.test.runner", + "android.test.base", + ], + test_suites: [ + "device-tests", + ], + + manifest: "AndroidManifest.xml", +} diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/AndroidManifest.xml b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/AndroidManifest.xml new file mode 100644 index 000000000000..617e8796317a --- /dev/null +++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/AndroidManifest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.overlaytest.overlayresapp" + xmlns:tools="http://schemas.android.com/tools"> + + <application> + <uses-library android:name="android.test.runner" /> + <activity + android:name=".OverlayResActivity" + android:exported="false" + android:configChanges="assetsPaths"> + </activity> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.overlaytest.overlayresapp" /> + +</manifest> diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/integers.xml b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/integers.xml new file mode 100644 index 000000000000..493333f8268b --- /dev/null +++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/integers.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <integer name="test_integer">0</integer> +</resources>
\ No newline at end of file diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/strings.xml b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/strings.xml new file mode 100644 index 000000000000..72820d37fdc7 --- /dev/null +++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <string name="app_name">My Application</string> + <string name="test_string">Test String</string> +</resources>
\ No newline at end of file diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResActivity.java b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResActivity.java new file mode 100644 index 000000000000..96143ae4e1e1 --- /dev/null +++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResActivity.java @@ -0,0 +1,50 @@ +/* + * 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.overlaytest.overlayresapp; + +import android.app.Activity; +import android.content.res.Configuration; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A test activity to verify that the assets paths configuration changes are received if the + * overlay targeting state is changed. + */ +public class OverlayResActivity extends Activity { + private Runnable mConfigurationChangedCallback; + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + final Runnable callback = mConfigurationChangedCallback; + if (callback != null) { + callback.run(); + } + } + + /** Registers the callback of onConfigurationChanged. */ + public void setConfigurationChangedCallback(Runnable callback) { + mConfigurationChangedCallback = callback; + } +} diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResTest.java b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResTest.java new file mode 100644 index 000000000000..1c377192f0ea --- /dev/null +++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResTest.java @@ -0,0 +1,148 @@ +/* + * 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.overlaytest.overlayresapp; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.Assert.assertNotNull; + +import static org.junit.Assert.fail; + +import android.content.Context; +import android.content.om.FabricatedOverlay; +import android.content.om.OverlayInfo; +import android.content.om.OverlayManager; +import android.content.om.OverlayManagerTransaction; +import android.content.res.Resources; +import android.os.UserHandle; +import android.util.TypedValue; + +import androidx.test.ext.junit.rules.ActivityScenarioRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@RunWith(JUnit4.class) +public class OverlayResTest { + // Default timeout value + private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5); + private static final String TEST_OVERLAY_NAME = "Test"; + private static final String TEST_RESOURCE_INTEGER = "integer/test_integer"; + private static final String TEST_RESOURCE_STRING = "string/test_string"; + private static final int TEST_INTEGER = 0; + private static final int TEST_FRRO_INTEGER = 1; + private static final String TEST_STRING = "Test String"; + private static final String TEST_FRRO_STRING = "FRRO Test String"; + private OverlayResActivity mActivity; + private Context mContext; + private OverlayManager mOverlayManager; + private int mUserId; + private UserHandle mUserHandle; + + @Rule + public ActivityScenarioRule<OverlayResActivity> mActivityScenarioRule = + new ActivityScenarioRule<>(OverlayResActivity.class); + + @Before + public void setUp() { + mActivityScenarioRule.getScenario().onActivity(activity -> { + assertThat(activity).isNotNull(); + mActivity = activity; + }); + mContext = mActivity.getApplicationContext(); + mOverlayManager = mContext.getSystemService(OverlayManager.class); + mUserId = UserHandle.myUserId(); + mUserHandle = UserHandle.of(mUserId); + } + + @After + public void tearDown() throws Exception { + final OverlayManagerTransaction.Builder cleanUp = new OverlayManagerTransaction.Builder(); + mOverlayManager.getOverlayInfosForTarget(mContext.getPackageName(), mUserHandle).forEach( + info -> { + if (info.isFabricated()) { + cleanUp.unregisterFabricatedOverlay(info.getOverlayIdentifier()); + } + }); + mOverlayManager.commit(cleanUp.build()); + + } + + @Test + public void overlayRes_onConfigurationChanged() throws Exception { + final CountDownLatch latch1 = new CountDownLatch(1); + mActivity.setConfigurationChangedCallback(() -> { + Resources r = mActivity.getApplicationContext().getResources(); + assertThat(r.getInteger(R.integer.test_integer)).isEqualTo(TEST_FRRO_INTEGER); + assertThat(r.getString(R.string.test_string)).isEqualTo(TEST_FRRO_STRING); + latch1.countDown(); + }); + + // Create and enable FRRO + final FabricatedOverlay overlay = new FabricatedOverlay.Builder( + mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName()) + .setResourceValue(TEST_RESOURCE_INTEGER, TypedValue.TYPE_INT_DEC, TEST_FRRO_INTEGER) + .setResourceValue(TEST_RESOURCE_STRING, TypedValue.TYPE_STRING, TEST_FRRO_STRING) + .build(); + + mOverlayManager.commit(new OverlayManagerTransaction.Builder() + .registerFabricatedOverlay(overlay) + .build()); + + OverlayInfo info = mOverlayManager.getOverlayInfo(overlay.getIdentifier(), mUserHandle); + assertNotNull(info); + assertThat(info.isEnabled()).isFalse(); + + mOverlayManager.commit(new OverlayManagerTransaction.Builder() + .setEnabled(overlay.getIdentifier(), true, mUserId) + .build()); + + info = mOverlayManager.getOverlayInfo(overlay.getIdentifier(), mUserHandle); + assertNotNull(info); + assertThat(info.isEnabled()).isTrue(); + + if (!latch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + fail("Fail to wait configuration changes for build and enabling frro, " + + "onConfigurationChanged() has not been invoked."); + } + + final CountDownLatch latch2 = new CountDownLatch(1); + mActivity.setConfigurationChangedCallback(() -> { + Resources r = mActivity.getApplicationContext().getResources(); + assertThat(r.getInteger(R.integer.test_integer)).isEqualTo(TEST_INTEGER); + assertThat(r.getString(R.string.test_string)).isEqualTo(TEST_STRING); + latch2.countDown(); + }); + + // unregister FRRO + mOverlayManager.commit(new OverlayManagerTransaction.Builder() + .unregisterFabricatedOverlay(overlay.getIdentifier()) + .build()); + + if (!latch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + fail("Fail to wait configuration changes after unregister frro," + + " onConfigurationChanged() has not been invoked."); + } + } +} diff --git a/core/tests/resourceflaggingtests/OWNERS b/core/tests/resourceflaggingtests/OWNERS new file mode 100644 index 000000000000..10950a193b25 --- /dev/null +++ b/core/tests/resourceflaggingtests/OWNERS @@ -0,0 +1 @@ +include /core/java/android/app/RESOURCES_OWNERS diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 9ea2943bc6da..1eb95c1efb08 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -70,10 +70,6 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { @NonNull private final TaskFragmentCallback mCallback; - @VisibleForTesting - @Nullable - TaskFragmentAnimationController mAnimationController; - /** * Callback that notifies the controller about changes to task fragments. */ @@ -91,25 +87,6 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { mCallback = callback; } - @Override - public void unregisterOrganizer() { - if (mAnimationController != null) { - mAnimationController.unregisterRemoteAnimations(); - mAnimationController = null; - } - super.unregisterOrganizer(); - } - - /** - * Overrides the animation for transitions of embedded activities organized by this organizer. - */ - void overrideSplitAnimation() { - if (mAnimationController == null) { - mAnimationController = new TaskFragmentAnimationController(this); - } - mAnimationController.registerRemoteAnimations(); - } - /** * Starts a new Activity and puts it into split with an existing Activity side-by-side. * @param launchingFragmentToken token for the launching TaskFragment. If it exists, it will diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 7ddda1f98809..b12072373c5d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -114,7 +114,6 @@ import java.util.function.BiConsumer; public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, ActivityEmbeddingComponent, DividerPresenter.DragEventCallback { static final String TAG = "SplitController"; - static final boolean ENABLE_SHELL_TRANSITIONS = true; // TODO(b/243518738): Move to WM Extensions if we have requirement of overlay without // association. It's not set in WM Extensions nor Wm Jetpack library currently. @@ -205,11 +204,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** Listener registered to {@link ClientTransactionListenerController}. */ @GuardedBy("mLock") - @Nullable + @NonNull private final BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener = - Flags.activityWindowInfoFlag() - ? this::onActivityWindowInfoChanged - : null; + this::onActivityWindowInfoChanged; private final Handler mHandler; private final MainThreadExecutor mExecutor; @@ -3097,20 +3094,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen public boolean isActivityEmbedded(@NonNull Activity activity) { Objects.requireNonNull(activity); synchronized (mLock) { - if (Flags.activityWindowInfoFlag()) { - final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity); - return activityWindowInfo != null && activityWindowInfo.isEmbedded(); - } - return mPresenter.isActivityEmbedded(activity.getActivityToken()); + final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity); + return activityWindowInfo != null && activityWindowInfo.isEmbedded(); } } @Override public void setEmbeddedActivityWindowInfoCallback(@NonNull Executor executor, @NonNull Consumer<EmbeddedActivityWindowInfo> callback) { - if (!Flags.activityWindowInfoFlag()) { - return; - } Objects.requireNonNull(executor); Objects.requireNonNull(callback); synchronized (mLock) { @@ -3124,9 +3115,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void clearEmbeddedActivityWindowInfoCallback() { - if (!Flags.activityWindowInfoFlag()) { - return; - } synchronized (mLock) { if (mEmbeddedActivityWindowInfoCallback == null) { return; @@ -3147,9 +3135,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Nullable @Override public EmbeddedActivityWindowInfo getEmbeddedActivityWindowInfo(@NonNull Activity activity) { - if (!Flags.activityWindowInfoFlag()) { - return null; - } synchronized (mLock) { final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity); return activityWindowInfo != null diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index eb1fc23d6b00..ea60b1531a3f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -166,11 +166,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mWindowLayoutComponent = windowLayoutComponent; mController = controller; registerOrganizer(); - if (!SplitController.ENABLE_SHELL_TRANSITIONS) { - // TODO(b/207070762): cleanup with legacy app transition - // Animation will be handled by WM Shell when Shell transition is enabled. - overrideSplitAnimation(); - } } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java deleted file mode 100644 index 33220c44a3b5..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.window.extensions.embedding; - -import static android.graphics.Matrix.MTRANS_X; -import static android.graphics.Matrix.MTRANS_Y; -import static android.view.RemoteAnimationTarget.MODE_CLOSING; - -import android.graphics.Point; -import android.graphics.Rect; -import android.view.Choreographer; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.animation.Animation; -import android.view.animation.Transformation; - -import androidx.annotation.NonNull; - -/** - * Wrapper to handle the TaskFragment animation update in one {@link SurfaceControl.Transaction}. - * - * The base adapter can be used for {@link RemoteAnimationTarget} that is simple open/close. - */ -class TaskFragmentAnimationAdapter { - - /** - * If {@link #mOverrideLayer} is set to this value, we don't want to override the surface layer. - */ - private static final int LAYER_NO_OVERRIDE = -1; - - @NonNull - final Animation mAnimation; - @NonNull - final RemoteAnimationTarget mTarget; - @NonNull - final SurfaceControl mLeash; - /** Area in absolute coordinate that the animation surface shouldn't go beyond. */ - @NonNull - private final Rect mWholeAnimationBounds = new Rect(); - /** - * Area in absolute coordinate that should represent all the content to show for this window. - * This should be the end bounds for opening window, and start bounds for closing window in case - * the window is resizing during the open/close transition. - */ - @NonNull - private final Rect mContentBounds = new Rect(); - /** Offset relative to the window parent surface for {@link #mContentBounds}. */ - @NonNull - private final Point mContentRelOffset = new Point(); - - @NonNull - final Transformation mTransformation = new Transformation(); - @NonNull - final float[] mMatrix = new float[9]; - @NonNull - final float[] mVecs = new float[4]; - @NonNull - final Rect mRect = new Rect(); - private boolean mIsFirstFrame = true; - private int mOverrideLayer = LAYER_NO_OVERRIDE; - - TaskFragmentAnimationAdapter(@NonNull Animation animation, - @NonNull RemoteAnimationTarget target) { - this(animation, target, target.leash, target.screenSpaceBounds); - } - - /** - * @param leash the surface to animate. - * @param wholeAnimationBounds area in absolute coordinate that the animation surface shouldn't - * go beyond. - */ - TaskFragmentAnimationAdapter(@NonNull Animation animation, - @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash, - @NonNull Rect wholeAnimationBounds) { - mAnimation = animation; - mTarget = target; - mLeash = leash; - mWholeAnimationBounds.set(wholeAnimationBounds); - if (target.mode == MODE_CLOSING) { - // When it is closing, we want to show the content at the start position in case the - // window is resizing as well. For example, when the activities is changing from split - // to stack, the bottom TaskFragment will be resized to fullscreen when hiding. - final Rect startBounds = target.startBounds; - final Rect endBounds = target.screenSpaceBounds; - mContentBounds.set(startBounds); - mContentRelOffset.set(target.localBounds.left, target.localBounds.top); - mContentRelOffset.offset( - startBounds.left - endBounds.left, - startBounds.top - endBounds.top); - } else { - mContentBounds.set(target.screenSpaceBounds); - mContentRelOffset.set(target.localBounds.left, target.localBounds.top); - } - } - - /** - * Surface layer to be set at the first frame of the animation. We will not set the layer if it - * is set to {@link #LAYER_NO_OVERRIDE}. - */ - final void overrideLayer(int layer) { - mOverrideLayer = layer; - } - - /** Called on frame update. */ - final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) { - if (mIsFirstFrame) { - t.show(mLeash); - if (mOverrideLayer != LAYER_NO_OVERRIDE) { - t.setLayer(mLeash, mOverrideLayer); - } - mIsFirstFrame = false; - } - - // Extract the transformation to the current time. - mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()), - mTransformation); - t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); - onAnimationUpdateInner(t); - } - - /** To be overridden by subclasses to adjust the animation surface change. */ - void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { - // Update the surface position and alpha. - mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y); - t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); - t.setAlpha(mLeash, mTransformation.getAlpha()); - - // Get current surface bounds in absolute coordinate. - // positionX/Y are in local coordinate, so minus the local offset to get the slide amount. - final int positionX = Math.round(mMatrix[MTRANS_X]); - final int positionY = Math.round(mMatrix[MTRANS_Y]); - final Rect cropRect = new Rect(mContentBounds); - cropRect.offset(positionX - mContentRelOffset.x, positionY - mContentRelOffset.y); - - // Store the current offset of the surface top left from (0,0) in absolute coordinate. - final int offsetX = cropRect.left; - final int offsetY = cropRect.top; - - // Intersect to make sure the animation happens within the whole animation bounds. - if (!cropRect.intersect(mWholeAnimationBounds)) { - // Hide the surface when it is outside of the animation area. - t.setAlpha(mLeash, 0); - } - - // cropRect is in absolute coordinate, so we need to translate it to surface top left. - cropRect.offset(-offsetX, -offsetY); - t.setCrop(mLeash, cropRect); - } - - /** Called after animation finished. */ - final void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { - onAnimationUpdate(t, mAnimation.getDuration()); - } - - final long getDurationHint() { - return mAnimation.computeDurationHint(); - } - - /** - * Should be used for the animation of the snapshot of a {@link RemoteAnimationTarget} that has - * size change. - */ - static class SnapshotAdapter extends TaskFragmentAnimationAdapter { - - SnapshotAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { - // Start leash is the snapshot of the starting surface. - super(animation, target, target.startLeash, target.screenSpaceBounds); - } - - @Override - void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { - // Snapshot should always be placed at the top left of the animation leash. - mTransformation.getMatrix().postTranslate(0, 0); - t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); - t.setAlpha(mLeash, mTransformation.getAlpha()); - } - } - - /** - * Should be used for the animation of the {@link RemoteAnimationTarget} that has size change. - */ - static class BoundsChangeAdapter extends TaskFragmentAnimationAdapter { - - BoundsChangeAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { - super(animation, target); - } - - @Override - void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { - mTransformation.getMatrix().postTranslate( - mTarget.localBounds.left, mTarget.localBounds.top); - t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); - t.setAlpha(mLeash, mTransformation.getAlpha()); - - // The following applies an inverse scale to the clip-rect so that it crops "after" the - // scale instead of before. - mVecs[1] = mVecs[2] = 0; - mVecs[0] = mVecs[3] = 1; - mTransformation.getMatrix().mapVectors(mVecs); - mVecs[0] = 1.f / mVecs[0]; - mVecs[3] = 1.f / mVecs[3]; - final Rect clipRect = mTransformation.getClipRect(); - mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f); - mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f); - mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f); - mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f); - t.setWindowCrop(mLeash, mRect); - } - } -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java deleted file mode 100644 index d7eb9a01f57c..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.window.extensions.embedding; - -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; - -import android.util.Log; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationDefinition; -import android.window.TaskFragmentOrganizer; - -import androidx.annotation.NonNull; - -import com.android.internal.annotations.VisibleForTesting; - -/** Controls the TaskFragment remote animations. */ -class TaskFragmentAnimationController { - - private static final String TAG = "TaskFragAnimationCtrl"; - static final boolean DEBUG = false; - - private final TaskFragmentOrganizer mOrganizer; - private final TaskFragmentAnimationRunner mRemoteRunner = new TaskFragmentAnimationRunner(); - @VisibleForTesting - final RemoteAnimationDefinition mDefinition; - private boolean mIsRegistered; - - TaskFragmentAnimationController(@NonNull TaskFragmentOrganizer organizer) { - mOrganizer = organizer; - mDefinition = new RemoteAnimationDefinition(); - final RemoteAnimationAdapter animationAdapter = - new RemoteAnimationAdapter(mRemoteRunner, 0, 0, true /* changeNeedsSnapshot */); - mDefinition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, animationAdapter); - mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, animationAdapter); - mDefinition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_CLOSE, animationAdapter); - mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, animationAdapter); - mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, animationAdapter); - } - - void registerRemoteAnimations() { - if (DEBUG) { - Log.v(TAG, "registerRemoteAnimations"); - } - if (mIsRegistered) { - return; - } - mOrganizer.registerRemoteAnimations(mDefinition); - mIsRegistered = true; - } - - void unregisterRemoteAnimations() { - if (DEBUG) { - Log.v(TAG, "unregisterRemoteAnimations"); - } - if (!mIsRegistered) { - return; - } - mOrganizer.unregisterRemoteAnimations(); - mIsRegistered = false; - } -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java deleted file mode 100644 index d9b73a8290f5..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.window.extensions.embedding; - -import static android.os.Process.THREAD_PRIORITY_DISPLAY; -import static android.view.RemoteAnimationTarget.MODE_CHANGING; -import static android.view.RemoteAnimationTarget.MODE_CLOSING; -import static android.view.RemoteAnimationTarget.MODE_OPENING; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; -import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; - -import android.animation.Animator; -import android.animation.ValueAnimator; -import android.graphics.Rect; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.RemoteException; -import android.util.Log; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.WindowManager; -import android.view.animation.Animation; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.BiFunction; - -/** To run the TaskFragment animations. */ -class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { - - private static final String TAG = "TaskFragAnimationRunner"; - private final Handler mHandler; - private final TaskFragmentAnimationSpec mAnimationSpec; - - TaskFragmentAnimationRunner() { - HandlerThread animationThread = new HandlerThread( - "androidx.window.extensions.embedding", THREAD_PRIORITY_DISPLAY); - animationThread.start(); - mHandler = animationThread.getThreadHandler(); - mAnimationSpec = new TaskFragmentAnimationSpec(mHandler); - } - - @Nullable - private Animator mAnimator; - - @Override - public void onAnimationStart(@WindowManager.TransitionOldType int transit, - @NonNull RemoteAnimationTarget[] apps, - @NonNull RemoteAnimationTarget[] wallpapers, - @NonNull RemoteAnimationTarget[] nonApps, - @NonNull IRemoteAnimationFinishedCallback finishedCallback) { - if (wallpapers.length != 0 || nonApps.length != 0) { - throw new IllegalArgumentException("TaskFragment shouldn't handle animation with" - + "wallpaper or non-app windows."); - } - if (TaskFragmentAnimationController.DEBUG) { - Log.v(TAG, "onAnimationStart transit=" + transit); - } - mHandler.post(() -> startAnimation(transit, apps, finishedCallback)); - } - - @Override - public void onAnimationCancelled() { - mHandler.post(this::cancelAnimation); - } - - /** Creates and starts animation. */ - private void startAnimation(@WindowManager.TransitionOldType int transit, - @NonNull RemoteAnimationTarget[] targets, - @NonNull IRemoteAnimationFinishedCallback finishedCallback) { - if (mAnimator != null) { - Log.w(TAG, "start new animation when the previous one is not finished yet."); - mAnimator.cancel(); - } - mAnimator = createAnimator(transit, targets, finishedCallback); - mAnimator.start(); - } - - /** Cancels animation. */ - private void cancelAnimation() { - if (mAnimator == null) { - return; - } - mAnimator.cancel(); - mAnimator = null; - } - - /** Creates the animator given the transition type and windows. */ - @NonNull - private Animator createAnimator(@WindowManager.TransitionOldType int transit, - @NonNull RemoteAnimationTarget[] targets, - @NonNull IRemoteAnimationFinishedCallback finishedCallback) { - final List<TaskFragmentAnimationAdapter> adapters = - createAnimationAdapters(transit, targets); - long duration = 0; - for (TaskFragmentAnimationAdapter adapter : adapters) { - duration = Math.max(duration, adapter.getDurationHint()); - } - final ValueAnimator animator = ValueAnimator.ofFloat(0, 1); - animator.setDuration(duration); - animator.addUpdateListener((anim) -> { - // Update all adapters in the same transaction. - final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - for (TaskFragmentAnimationAdapter adapter : adapters) { - adapter.onAnimationUpdate(t, animator.getCurrentPlayTime()); - } - t.apply(); - }); - animator.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) {} - - @Override - public void onAnimationEnd(Animator animation) { - final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - for (TaskFragmentAnimationAdapter adapter : adapters) { - adapter.onAnimationEnd(t); - } - t.apply(); - - try { - finishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } - mAnimator = null; - } - - @Override - public void onAnimationCancel(Animator animation) {} - - @Override - public void onAnimationRepeat(Animator animation) {} - }); - return animator; - } - - /** List of {@link TaskFragmentAnimationAdapter} to handle animations on all window targets. */ - @NonNull - private List<TaskFragmentAnimationAdapter> createAnimationAdapters( - @WindowManager.TransitionOldType int transit, - @NonNull RemoteAnimationTarget[] targets) { - switch (transit) { - case TRANSIT_OLD_ACTIVITY_OPEN: - case TRANSIT_OLD_TASK_FRAGMENT_OPEN: - return createOpenAnimationAdapters(targets); - case TRANSIT_OLD_ACTIVITY_CLOSE: - case TRANSIT_OLD_TASK_FRAGMENT_CLOSE: - return createCloseAnimationAdapters(targets); - case TRANSIT_OLD_TASK_FRAGMENT_CHANGE: - return createChangeAnimationAdapters(targets); - default: - throw new IllegalArgumentException("Unhandled transit type=" + transit); - } - } - - @NonNull - private List<TaskFragmentAnimationAdapter> createOpenAnimationAdapters( - @NonNull RemoteAnimationTarget[] targets) { - return createOpenCloseAnimationAdapters(targets, true /* isOpening */, - mAnimationSpec::loadOpenAnimation); - } - - @NonNull - private List<TaskFragmentAnimationAdapter> createCloseAnimationAdapters( - @NonNull RemoteAnimationTarget[] targets) { - return createOpenCloseAnimationAdapters(targets, false /* isOpening */, - mAnimationSpec::loadCloseAnimation); - } - - /** - * Creates {@link TaskFragmentAnimationAdapter} for OPEN and CLOSE types of transition. - * @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type. - */ - @NonNull - private List<TaskFragmentAnimationAdapter> createOpenCloseAnimationAdapters( - @NonNull RemoteAnimationTarget[] targets, boolean isOpening, - @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider) { - // We need to know if the target window is only a partial of the whole animation screen. - // If so, we will need to adjust it to make the whole animation screen looks like one. - final List<RemoteAnimationTarget> openingTargets = new ArrayList<>(); - final List<RemoteAnimationTarget> closingTargets = new ArrayList<>(); - final Rect openingWholeScreenBounds = new Rect(); - final Rect closingWholeScreenBounds = new Rect(); - for (RemoteAnimationTarget target : targets) { - if (target.mode != MODE_CLOSING) { - openingTargets.add(target); - openingWholeScreenBounds.union(target.screenSpaceBounds); - } else { - closingTargets.add(target); - closingWholeScreenBounds.union(target.screenSpaceBounds); - // Union the start bounds since this may be the ClosingChanging animation. - closingWholeScreenBounds.union(target.startBounds); - } - } - - // For OPEN transition, open windows should be above close windows. - // For CLOSE transition, open windows should be below close windows. - int offsetLayer = TYPE_LAYER_OFFSET; - final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>(); - for (RemoteAnimationTarget target : openingTargets) { - final TaskFragmentAnimationAdapter adapter = createOpenCloseAnimationAdapter(target, - animationProvider, openingWholeScreenBounds); - if (isOpening) { - adapter.overrideLayer(offsetLayer++); - } - adapters.add(adapter); - } - for (RemoteAnimationTarget target : closingTargets) { - final TaskFragmentAnimationAdapter adapter = createOpenCloseAnimationAdapter(target, - animationProvider, closingWholeScreenBounds); - if (!isOpening) { - adapter.overrideLayer(offsetLayer++); - } - adapters.add(adapter); - } - return adapters; - } - - @NonNull - private TaskFragmentAnimationAdapter createOpenCloseAnimationAdapter( - @NonNull RemoteAnimationTarget target, - @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider, - @NonNull Rect wholeAnimationBounds) { - final Animation animation = animationProvider.apply(target, wholeAnimationBounds); - return new TaskFragmentAnimationAdapter(animation, target, target.leash, - wholeAnimationBounds); - } - - @NonNull - private List<TaskFragmentAnimationAdapter> createChangeAnimationAdapters( - @NonNull RemoteAnimationTarget[] targets) { - if (shouldUseJumpCutForChangeAnimation(targets)) { - return new ArrayList<>(); - } - - final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>(); - for (RemoteAnimationTarget target : targets) { - if (target.mode == MODE_CHANGING) { - // This is the target with bounds change. - final Animation[] animations = - mAnimationSpec.createChangeBoundsChangeAnimations(target); - // Adapter for the starting snapshot leash. - adapters.add(new TaskFragmentAnimationAdapter.SnapshotAdapter( - animations[0], target)); - // Adapter for the ending bounds changed leash. - adapters.add(new TaskFragmentAnimationAdapter.BoundsChangeAdapter( - animations[1], target)); - continue; - } - - // These are the other targets that don't have bounds change in the same transition. - final Animation animation; - if (target.hasAnimatingParent) { - // No-op if it will be covered by the changing parent window. - animation = TaskFragmentAnimationSpec.createNoopAnimation(target); - } else if (target.mode == MODE_CLOSING) { - animation = mAnimationSpec.createChangeBoundsCloseAnimation(target); - } else { - animation = mAnimationSpec.createChangeBoundsOpenAnimation(target); - } - adapters.add(new TaskFragmentAnimationAdapter(animation, target)); - } - return adapters; - } - - /** - * Whether we should use jump cut for the change transition. - * This normally happens when opening a new secondary with the existing primary using a - * different split layout. This can be complicated, like from horizontal to vertical split with - * new split pairs. - * Uses a jump cut animation to simplify. - */ - private boolean shouldUseJumpCutForChangeAnimation(@NonNull RemoteAnimationTarget[] targets) { - boolean hasOpeningWindow = false; - boolean hasClosingWindow = false; - for (RemoteAnimationTarget target : targets) { - if (target.hasAnimatingParent) { - continue; - } - hasOpeningWindow |= target.mode == MODE_OPENING; - hasClosingWindow |= target.mode == MODE_CLOSING; - } - return hasOpeningWindow && hasClosingWindow; - } -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java deleted file mode 100644 index 1f866c3b99c9..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.window.extensions.embedding; - -import static android.view.RemoteAnimationTarget.MODE_CLOSING; - -import android.app.ActivityThread; -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.graphics.Rect; -import android.os.Handler; -import android.provider.Settings; -import android.view.RemoteAnimationTarget; -import android.view.WindowManager; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.view.animation.AnimationSet; -import android.view.animation.AnimationUtils; -import android.view.animation.ClipRectAnimation; -import android.view.animation.Interpolator; -import android.view.animation.LinearInterpolator; -import android.view.animation.ScaleAnimation; -import android.view.animation.TranslateAnimation; - -import androidx.annotation.NonNull; - -import com.android.internal.R; -import com.android.internal.policy.AttributeCache; -import com.android.internal.policy.TransitionAnimation; - -/** Animation spec for TaskFragment transition. */ -// TODO(b/206557124): provide an easier way to customize animation -class TaskFragmentAnimationSpec { - - private static final String TAG = "TaskFragAnimationSpec"; - private static final int CHANGE_ANIMATION_DURATION = 517; - private static final int CHANGE_ANIMATION_FADE_DURATION = 80; - private static final int CHANGE_ANIMATION_FADE_OFFSET = 30; - - private final Context mContext; - private final TransitionAnimation mTransitionAnimation; - private final Interpolator mFastOutExtraSlowInInterpolator; - private final LinearInterpolator mLinearInterpolator; - private float mTransitionAnimationScaleSetting; - - TaskFragmentAnimationSpec(@NonNull Handler handler) { - mContext = ActivityThread.currentActivityThread().getApplication(); - mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG); - // Initialize the AttributeCache for the TransitionAnimation. - AttributeCache.init(mContext); - mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator( - mContext, android.R.interpolator.fast_out_extra_slow_in); - mLinearInterpolator = new LinearInterpolator(); - - // The transition animation should be adjusted based on the developer option. - final ContentResolver resolver = mContext.getContentResolver(); - mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); - resolver.registerContentObserver( - Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false, - new SettingsObserver(handler)); - } - - /** For target that doesn't need to be animated. */ - @NonNull - static Animation createNoopAnimation(@NonNull RemoteAnimationTarget target) { - // Noop but just keep the target showing/hiding. - final float alpha = target.mode == MODE_CLOSING ? 0f : 1f; - return new AlphaAnimation(alpha, alpha); - } - - /** Animation for target that is opening in a change transition. */ - @NonNull - Animation createChangeBoundsOpenAnimation(@NonNull RemoteAnimationTarget target) { - final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds(); - final Rect bounds = target.screenSpaceBounds; - final int startLeft; - final int startTop; - if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) { - // The window will be animated in from left or right depending on its position. - startTop = 0; - startLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width(); - } else { - // The window will be animated in from top or bottom depending on its position. - startTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height(); - startLeft = 0; - } - - // The position should be 0-based as we will post translate in - // TaskFragmentAnimationAdapter#onAnimationUpdate - final Animation animation = new TranslateAnimation(startLeft, 0, startTop, 0); - animation.setInterpolator(mFastOutExtraSlowInInterpolator); - animation.setDuration(CHANGE_ANIMATION_DURATION); - animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height()); - animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); - return animation; - } - - /** Animation for target that is closing in a change transition. */ - @NonNull - Animation createChangeBoundsCloseAnimation(@NonNull RemoteAnimationTarget target) { - final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds(); - // Use startBounds if the window is closing in case it may also resize. - final Rect bounds = target.startBounds; - final int endTop; - final int endLeft; - if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) { - // The window will be animated out to left or right depending on its position. - endTop = 0; - endLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width(); - } else { - // The window will be animated out to top or bottom depending on its position. - endTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height(); - endLeft = 0; - } - - // The position should be 0-based as we will post translate in - // TaskFragmentAnimationAdapter#onAnimationUpdate - final Animation animation = new TranslateAnimation(0, endLeft, 0, endTop); - animation.setInterpolator(mFastOutExtraSlowInInterpolator); - animation.setDuration(CHANGE_ANIMATION_DURATION); - animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height()); - animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); - return animation; - } - - /** - * Animation for target that is changing (bounds change) in a change transition. - * @return the return array always has two elements. The first one is for the start leash, and - * the second one is for the end leash. - */ - @NonNull - Animation[] createChangeBoundsChangeAnimations(@NonNull RemoteAnimationTarget target) { - // Both start bounds and end bounds are in screen coordinates. We will post translate - // to the local coordinates in TaskFragmentAnimationAdapter#onAnimationUpdate - final Rect startBounds = target.startBounds; - final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds(); - final Rect endBounds = target.screenSpaceBounds; - float scaleX = ((float) startBounds.width()) / endBounds.width(); - float scaleY = ((float) startBounds.height()) / endBounds.height(); - // Start leash is a child of the end leash. Reverse the scale so that the start leash won't - // be scaled up with its parent. - float startScaleX = 1.f / scaleX; - float startScaleY = 1.f / scaleY; - - // The start leash will be fade out. - final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */); - final Animation startAlpha = new AlphaAnimation(1f, 0f); - startAlpha.setInterpolator(mLinearInterpolator); - startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION); - startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET); - startSet.addAnimation(startAlpha); - final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY, - startScaleY); - startScale.setInterpolator(mFastOutExtraSlowInInterpolator); - startScale.setDuration(CHANGE_ANIMATION_DURATION); - startSet.addAnimation(startScale); - startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(), - endBounds.height()); - startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting); - - // The end leash will be moved into the end position while scaling. - final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */); - endSet.setInterpolator(mFastOutExtraSlowInInterpolator); - final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1); - endScale.setDuration(CHANGE_ANIMATION_DURATION); - endSet.addAnimation(endScale); - // The position should be 0-based as we will post translate in - // TaskFragmentAnimationAdapter#onAnimationUpdate - final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0, - startBounds.top - endBounds.top, 0); - endTranslate.setDuration(CHANGE_ANIMATION_DURATION); - endSet.addAnimation(endTranslate); - // The end leash is resizing, we should update the window crop based on the clip rect. - final Rect startClip = new Rect(startBounds); - final Rect endClip = new Rect(endBounds); - startClip.offsetTo(0, 0); - endClip.offsetTo(0, 0); - final Animation clipAnim = new ClipRectAnimation(startClip, endClip); - clipAnim.setDuration(CHANGE_ANIMATION_DURATION); - endSet.addAnimation(clipAnim); - endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(), - parentBounds.height()); - endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting); - - return new Animation[]{startSet, endSet}; - } - - @NonNull - Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target, - @NonNull Rect wholeAnimationBounds) { - final boolean isEnter = target.mode != MODE_CLOSING; - final Animation animation; - // Background color on TaskDisplayArea has already been set earlier in - // WindowContainer#getAnimationAdapter. - if (target.showBackdrop) { - animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter - ? com.android.internal.R.anim.task_fragment_clear_top_open_enter - : com.android.internal.R.anim.task_fragment_clear_top_open_exit); - } else { - animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter - ? com.android.internal.R.anim.task_fragment_open_enter - : com.android.internal.R.anim.task_fragment_open_exit); - } - // Use the whole animation bounds instead of the change bounds, so that when multiple change - // targets are opening at the same time, the animation applied to each will be the same. - // Otherwise, we may see gap between the activities that are launching together. - animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(), - wholeAnimationBounds.width(), wholeAnimationBounds.height()); - animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); - return animation; - } - - @NonNull - Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target, - @NonNull Rect wholeAnimationBounds) { - final boolean isEnter = target.mode != MODE_CLOSING; - final Animation animation; - if (target.showBackdrop) { - animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter - ? com.android.internal.R.anim.task_fragment_clear_top_close_enter - : com.android.internal.R.anim.task_fragment_clear_top_close_exit); - } else { - animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter - ? com.android.internal.R.anim.task_fragment_close_enter - : com.android.internal.R.anim.task_fragment_close_exit); - } - // Use the whole animation bounds instead of the change bounds, so that when multiple change - // targets are closing at the same time, the animation applied to each will be the same. - // Otherwise, we may see gap between the activities that are finishing together. - animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(), - wholeAnimationBounds.width(), wholeAnimationBounds.height()); - animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); - return animation; - } - - private float getTransitionAnimationScaleSetting() { - return WindowManager.fixScale(Settings.Global.getFloat(mContext.getContentResolver(), - Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat( - R.dimen.config_appTransitionAnimationDurationScaleDefault))); - } - - private class SettingsObserver extends ContentObserver { - SettingsObserver(@NonNull Handler handler) { - super(handler); - } - - @Override - public void onChange(boolean selfChange) { - mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); - } - } -} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 7b473b04548c..ad41b18dcbc6 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -23,8 +23,6 @@ import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTest import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -86,24 +84,6 @@ public class JetpackTaskFragmentOrganizerTest { } @Test - public void testUnregisterOrganizer() { - mOrganizer.overrideSplitAnimation(); - mOrganizer.unregisterOrganizer(); - - verify(mOrganizer).unregisterRemoteAnimations(); - } - - @Test - public void testOverrideSplitAnimation() { - assertNull(mOrganizer.mAnimationController); - - mOrganizer.overrideSplitAnimation(); - - assertNotNull(mOrganizer.mAnimationController); - verify(mOrganizer).registerRemoteAnimations(mOrganizer.mAnimationController.mDefinition); - } - - @Test public void testExpandTaskFragment() { final TaskContainer taskContainer = createTestTaskContainer(); doReturn(taskContainer).when(mSplitController).getTaskContainer(anyInt()); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index efeec82b782e..99c0ee29962c 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -103,8 +103,6 @@ import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.extensions.layout.WindowLayoutComponentImpl; import androidx.window.extensions.layout.WindowLayoutInfo; -import com.android.window.flags.Flags; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -1557,8 +1555,6 @@ public class SplitControllerTest { @Test public void testIsActivityEmbedded() { - mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); - assertFalse(mSplitController.isActivityEmbedded(mActivity)); doReturn(true).when(mActivityWindowInfo).isEmbedded(); @@ -1568,8 +1564,6 @@ public class SplitControllerTest { @Test public void testGetEmbeddedActivityWindowInfo() { - mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); - final boolean isEmbedded = true; final Rect taskBounds = new Rect(0, 0, 1000, 2000); final Rect activityStackBounds = new Rect(0, 0, 500, 2000); @@ -1584,8 +1578,6 @@ public class SplitControllerTest { @Test public void testSetEmbeddedActivityWindowInfoCallback() { - mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); - final ClientTransactionListenerController controller = ClientTransactionListenerController .getInstance(); spyOn(controller); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java deleted file mode 100644 index a1e9f08585f6..000000000000 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.window.extensions.embedding; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; - -import static org.mockito.Mockito.never; - -import android.platform.test.annotations.Presubmit; -import android.window.TaskFragmentOrganizer; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -/** - * Test class for {@link TaskFragmentAnimationController}. - * - * Build/Install/Run: - * atest WMJetpackUnitTests:TaskFragmentAnimationControllerTest - */ -@Presubmit -@SmallTest -@RunWith(AndroidJUnit4.class) -public class TaskFragmentAnimationControllerTest { - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - - @Mock - private TaskFragmentOrganizer mOrganizer; - private TaskFragmentAnimationController mAnimationController; - - @Before - public void setup() { - mAnimationController = new TaskFragmentAnimationController(mOrganizer); - } - - @Test - public void testRegisterRemoteAnimations() { - mAnimationController.registerRemoteAnimations(); - - verify(mOrganizer).registerRemoteAnimations(mAnimationController.mDefinition); - - mAnimationController.registerRemoteAnimations(); - - // No extra call if it has been registered. - verify(mOrganizer).registerRemoteAnimations(mAnimationController.mDefinition); - } - - @Test - public void testUnregisterRemoteAnimations() { - mAnimationController.unregisterRemoteAnimations(); - - // No call if it is not registered. - verify(mOrganizer, never()).unregisterRemoteAnimations(); - - mAnimationController.registerRemoteAnimations(); - mAnimationController.unregisterRemoteAnimations(); - - verify(mOrganizer).unregisterRemoteAnimations(); - - mAnimationController.unregisterRemoteAnimations(); - - // No extra call if it has been unregistered. - verify(mOrganizer).unregisterRemoteAnimations(); - } -} diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 3ff40e0886a4..56ea7c201e53 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -113,16 +113,6 @@ flag { } flag { - name: "animate_bubble_size_change" - namespace: "multitasking" - description: "Turns on the animation for bubble bar icons size change" - bug: "335575529" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "enable_taskbar_on_phones" namespace: "multitasking" description: "Enables taskbar on phones" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index 419d5c0af1a4..864f7cd421ee 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -19,6 +19,10 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="@dimen/desktop_mode_handle_menu_width" android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" + android:paddingBottom="@dimen/desktop_mode_handle_menu_pill_elevation" + android:paddingRight="@dimen/desktop_mode_handle_menu_pill_elevation" android:orientation="vertical"> <LinearLayout @@ -27,7 +31,7 @@ android:layout_height="@dimen/desktop_mode_handle_menu_app_info_pill_height" android:layout_marginTop="@dimen/desktop_mode_handle_menu_margin_top" android:layout_marginStart="1dp" - android:elevation="1dp" + android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:orientation="horizontal" android:background="@drawable/desktop_mode_decor_handle_menu_background" android:gravity="center_vertical"> @@ -73,7 +77,7 @@ android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" android:layout_marginStart="1dp" android:orientation="horizontal" - android:elevation="1dp" + android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:background="@drawable/desktop_mode_decor_handle_menu_background" android:gravity="center_vertical"> @@ -124,7 +128,7 @@ android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" android:layout_marginStart="1dp" android:orientation="vertical" - android:elevation="1dp" + android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:background="@drawable/desktop_mode_decor_handle_menu_background"> <Button @@ -143,7 +147,7 @@ android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" android:layout_marginStart="1dp" android:orientation="vertical" - android:elevation="1dp" + android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:background="@drawable/desktop_mode_decor_handle_menu_background"> <Button diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index d143263b69a5..269a58693a24 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -495,9 +495,16 @@ <!-- The radius of the Maximize menu shadow. --> <dimen name="desktop_mode_maximize_menu_shadow_radius">8dp</dimen> - <!-- The width of the handle menu in desktop mode. --> + <!-- The width of the handle menu in desktop mode. --> <dimen name="desktop_mode_handle_menu_width">216dp</dimen> + <!-- The maximum height of the handle menu in desktop mode. Four pills (52dp each) plus 2dp + spacing between them plus 4dp top padding. --> + <dimen name="desktop_mode_handle_menu_height">218dp</dimen> + + <!-- The elevation set on the handle menu pills. --> + <dimen name="desktop_mode_handle_menu_pill_elevation">1dp</dimen> + <!-- The height of the handle menu's "App Info" pill in desktop mode. --> <dimen name="desktop_mode_handle_menu_app_info_pill_height">52dp</dimen> @@ -510,8 +517,8 @@ <!-- The height of the handle menu's "Open in browser" pill in desktop mode. --> <dimen name="desktop_mode_handle_menu_open_in_browser_pill_height">52dp</dimen> - <!-- The height of the handle menu in desktop mode. --> - <dimen name="desktop_mode_handle_menu_height">380dp</dimen> + <!-- The margin between pills of the handle menu in desktop mode. --> + <dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen> <!-- The top margin of the handle menu in desktop mode. --> <dimen name="desktop_mode_handle_menu_margin_top">4dp</dimen> @@ -519,9 +526,6 @@ <!-- The start margin of the handle menu in desktop mode. --> <dimen name="desktop_mode_handle_menu_margin_start">6dp</dimen> - <!-- The margin between pills of the handle menu in desktop mode. --> - <dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen> - <!-- The radius of the caption menu corners. --> <dimen name="desktop_mode_handle_menu_corner_radius">26dp</dimen> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index ece02711070e..8467e972526e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -422,6 +422,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @VisibleForTesting public void onThresholdCrossed() { mThresholdCrossed = true; + // There was no focus window when calling startBackNavigation, still pilfer pointers so + // the next focus window won't receive motion events. + if (mBackNavigationInfo == null) { + tryPilferPointers(); + return; + } // Dispatch onBackStarted, only to app callbacks. // System callbacks will receive onBackStarted when the remote animation starts. final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(); @@ -542,6 +548,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (backNavigationInfo == null) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null."); cancelLatencyTracking(); + tryPilferPointers(); return; } final int backType = backNavigationInfo.getType(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java index bfee820870f1..736d954513b1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java @@ -54,4 +54,11 @@ public class HandlerExecutor implements ShellExecutor { public boolean hasCallback(Runnable r) { return mHandler.hasCallbacks(r); } + + @Override + public void assertCurrentThread() { + if (!mHandler.getLooper().isCurrentThread()) { + throw new IllegalStateException("must be called on " + mHandler); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java index f729164ed303..2c2961fd4b65 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java @@ -96,4 +96,11 @@ public interface ShellExecutor extends Executor { * See {@link android.os.Handler#hasCallbacks(Runnable)}. */ boolean hasCallback(Runnable runnable); + + /** + * May throw if the caller is not on the same thread as the executor. + */ + default void assertCurrentThread() { + return; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index eeceaa943af2..45feff5b0ccc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -245,7 +245,10 @@ public abstract class WMShellModule { return new CaptionWindowDecorViewModel( context, mainHandler, + mainExecutor, mainChoreographer, + windowManager, + shellInit, taskOrganizer, displayController, rootTaskDisplayAreaOrganizer, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java index 240cf3b96e89..037fbb235bd4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java @@ -38,12 +38,10 @@ import com.android.wm.shell.common.pip.PipMediaController; import com.android.wm.shell.common.pip.PipPerfHintController; import com.android.wm.shell.common.pip.PipSnapAlgorithm; import com.android.wm.shell.common.pip.PipUiEventLogger; -import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.onehanded.OneHandedController; -import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; @@ -79,7 +77,7 @@ import java.util.Optional; public abstract class Pip1Module { @WMSingleton @Provides - static Optional<Pip> providePip1(Context context, + static Optional<PipController.PipImpl> providePip1(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, ShellController shellController, @@ -104,20 +102,16 @@ public abstract class Pip1Module { TabletopModeController pipTabletopController, Optional<OneHandedController> oneHandedController, @ShellMainThread ShellExecutor mainExecutor) { - if (PipUtils.isPip2ExperimentEnabled()) { - return Optional.empty(); - } else { - return Optional.ofNullable(PipController.create( - context, shellInit, shellCommandHandler, shellController, - displayController, pipAnimationController, pipAppOpsListener, - pipBoundsAlgorithm, - pipKeepClearAlgorithm, pipBoundsState, pipDisplayLayoutState, - pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, - pipTransitionState, pipTouchHandler, pipTransitionController, - windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - displayInsetsController, pipTabletopController, oneHandedController, - mainExecutor)); - } + return Optional.ofNullable(PipController.create( + context, shellInit, shellCommandHandler, shellController, + displayController, pipAnimationController, pipAppOpsListener, + pipBoundsAlgorithm, + pipKeepClearAlgorithm, pipBoundsState, pipDisplayLayoutState, + pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, + pipTransitionState, pipTouchHandler, pipTransitionController, + windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, + displayInsetsController, pipTabletopController, oneHandedController, + mainExecutor)); } // Handler is used by Icon.loadDrawableAsync 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 696831747865..ea7e9685dd92 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 @@ -81,6 +81,16 @@ public abstract class Pip2Module { @WMSingleton @Provides + static Optional<PipController.PipImpl> providePip2(Optional<PipController> pipController) { + if (pipController.isEmpty()) { + return Optional.empty(); + } else { + return Optional.ofNullable(pipController.get().getPipImpl()); + } + } + + @WMSingleton + @Provides static Optional<PipController> providePipController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java index f2631eff890d..a3afe7860f2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java @@ -18,12 +18,16 @@ package com.android.wm.shell.dagger.pip; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip2.phone.PipController; import com.android.wm.shell.pip2.phone.PipTransition; import dagger.Module; import dagger.Provides; +import java.util.Optional; + /** * Provides dependencies for external components / modules reference PiP and extracts away the * selection of legacy and new PiP implementation. @@ -44,4 +48,17 @@ public abstract class PipModule { return legacyPipTransition; } } + + @WMSingleton + @Provides + static Optional<Pip> providePip( + Optional<com.android.wm.shell.pip.phone.PipController.PipImpl> pip1, + Optional<PipController.PipImpl> pip2) { + if (PipUtils.isPip2ExperimentEnabled()) { + return Optional.ofNullable(pip2.orElse(null)); + + } else { + return Optional.ofNullable(pip1.orElse(null)); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 18157d6255e3..580724666949 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -159,18 +159,6 @@ class DesktopTasksController( } } - private val transitionAreaHeight - get() = - context.resources.getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height - ) - - private val transitionAreaWidth - get() = - context.resources.getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_transition_area_width - ) - /** Task id of the task currently being dragged from fullscreen/split. */ val draggingTaskId get() = dragToDesktopTransitionHandler.draggingTaskId @@ -229,6 +217,15 @@ class DesktopTasksController( dragToDesktopTransitionHandler.setSplitScreenController(controller) } + /** Returns the transition type for the given remote transition. */ + private fun transitionType(remoteTransition: RemoteTransition?): Int { + if (remoteTransition == null) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: remoteTransition is null") + return TRANSIT_NONE + } + return TRANSIT_TO_FRONT + } + /** Show all tasks, that are part of the desktop, on top of launcher */ fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) { ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps") @@ -236,8 +233,7 @@ class DesktopTasksController( bringDesktopAppsToFront(displayId, wct) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - // TODO(b/309014605): ensure remote transition is supplied once state is introduced - val transitionType = if (remoteTransition == null) TRANSIT_NONE else TRANSIT_TO_FRONT + val transitionType = transitionType(remoteTransition) val handler = remoteTransition?.let { OneShotRemoteHandler(transitions.mainExecutor, remoteTransition) @@ -776,12 +772,13 @@ class DesktopTasksController( newTaskIdInFront ?: "null" ) - if (Flags.enableDesktopWindowingWallpaperActivity()) { + // Move home to front, ensures that we go back home when all desktop windows are closed + moveHomeTask(wct, toTop = true) + + // Currently, we only handle the desktop on the default display really. + if (displayId == DEFAULT_DISPLAY && Flags.enableDesktopWindowingWallpaperActivity()) { // Add translucent wallpaper activity to show the wallpaper underneath addWallpaperActivity(wct) - } else { - // Move home to front - moveHomeTask(wct, toTop = true) } val nonMinimizedTasksOrderedFrontToBack = @@ -1110,6 +1107,10 @@ class DesktopTasksController( if (useDesktopOverrideDensity()) { wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } + if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) { + // Remove wallpaper activity when leaving desktop mode + removeWallpaperActivity(wct) + } } /** @@ -1125,6 +1126,10 @@ class DesktopTasksController( // The task's density may have been overridden in freeform; revert it here as we don't // want it overridden in multi-window. wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) + if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) { + // Remove wallpaper activity when leaving desktop mode + removeWallpaperActivity(wct) + } } /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md index 3572d161f5b9..84f6af4125b8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -68,7 +68,7 @@ adb shell dumpsys SurfaceFlinger ## Tracing global SurfaceControl transaction updates While Winscope traces are very useful, it sometimes doesn't give you enough information about which -part of the code is initiating the transaction updates. In such cases, it can be helpful to get +part of the code is initiating the transaction updates. In such cases, it can be helpful to get stack traces when specific surface transaction calls are made, which is possible by enabling the following system properties for example: ```shell @@ -81,9 +81,11 @@ adb logcat -s "SurfaceControlRegistry" # Disabling logging adb shell setprop persist.wm.debug.sc.tx.log_match_call \"\" adb shell setprop persist.wm.debug.sc.tx.log_match_name \"\" -adb reboot ``` +A reboot is required to enable the logging. Once enabled, reboot is not needed to update the +properties. + It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite noisy if unfiltered. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index d1d82755d12c..0cb7e17e41e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -374,7 +374,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb * Instantiates {@link PipController}, returns {@code null} if the feature not supported. */ @Nullable - public static Pip create(Context context, + public static PipImpl create(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, ShellController shellController, @@ -1177,7 +1177,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb /** * The interface for calls from outside the Shell, within the host process. */ - private class PipImpl implements Pip { + public class PipImpl implements Pip { @Override public void expandPip() { mMainExecutor.execute(() -> { 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 06adad626e9f..c12219c8a9e0 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 @@ -55,6 +55,8 @@ import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -62,6 +64,8 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import java.io.PrintWriter; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Manages the picture-in-picture (PIP) UI and states for Phones. @@ -86,6 +90,8 @@ public class PipController implements ConfigurationChangeListener, private final ShellTaskOrganizer mShellTaskOrganizer; private final PipTransitionState mPipTransitionState; private final ShellExecutor mMainExecutor; + private final PipImpl mImpl; + private Consumer<Boolean> mOnIsInPipStateChangedListener; // Wrapper for making Binder calls into PiP animation listener hosted in launcher's Recents. private PipAnimationListener mPipRecentsAnimationListener; @@ -140,6 +146,7 @@ public class PipController implements ConfigurationChangeListener, mPipTransitionState = pipTransitionState; mPipTransitionState.addPipTransitionStateChangedListener(this); mMainExecutor = mainExecutor; + mImpl = new PipImpl(); if (PipUtils.isPip2ExperimentEnabled()) { shellInit.addInitCallback(this::onInit, this); @@ -174,6 +181,10 @@ public class PipController implements ConfigurationChangeListener, pipTransitionState, mainExecutor); } + public PipImpl getPipImpl() { + return mImpl; + } + 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 @@ -310,22 +321,29 @@ public class PipController implements ConfigurationChangeListener, @Override public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { - if (newState == PipTransitionState.SWIPING_TO_PIP) { - Preconditions.checkState(extra != null, - "No extra bundle for " + mPipTransitionState); - - SurfaceControl overlay = extra.getParcelable( - SWIPE_TO_PIP_OVERLAY, SurfaceControl.class); - Rect appBounds = extra.getParcelable( - SWIPE_TO_PIP_APP_BOUNDS, Rect.class); - - Preconditions.checkState(appBounds != null, - "App bounds can't be null for " + mPipTransitionState); - mPipTransitionState.setSwipePipToHomeState(overlay, appBounds); - } else if (newState == PipTransitionState.ENTERED_PIP) { - if (mPipTransitionState.isInSwipePipToHomeTransition()) { - mPipTransitionState.resetSwipePipToHomeState(); - } + switch (newState) { + case PipTransitionState.SWIPING_TO_PIP: + Preconditions.checkState(extra != null, + "No extra bundle for " + mPipTransitionState); + + SurfaceControl overlay = extra.getParcelable( + SWIPE_TO_PIP_OVERLAY, SurfaceControl.class); + Rect appBounds = extra.getParcelable( + SWIPE_TO_PIP_APP_BOUNDS, Rect.class); + + Preconditions.checkState(appBounds != null, + "App bounds can't be null for " + mPipTransitionState); + mPipTransitionState.setSwipePipToHomeState(overlay, appBounds); + break; + case PipTransitionState.ENTERED_PIP: + if (mPipTransitionState.isInSwipePipToHomeTransition()) { + mPipTransitionState.resetSwipePipToHomeState(); + } + mOnIsInPipStateChangedListener.accept(true /* inPip */); + break; + case PipTransitionState.EXITED_PIP: + mOnIsInPipStateChangedListener.accept(false /* inPip */); + break; } } @@ -355,6 +373,53 @@ public class PipController implements ConfigurationChangeListener, mPipTransitionState.dump(pw, innerPrefix); } + private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + mOnIsInPipStateChangedListener = callback; + if (mOnIsInPipStateChangedListener != null) { + callback.accept(mPipTransitionState.isInPip()); + } + } + + /** + * The interface for calls from outside the Shell, within the host process. + */ + public class PipImpl implements Pip { + @Override + public void expandPip() {} + + @Override + public void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) {} + + @Override + public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + mMainExecutor.execute(() -> { + PipController.this.setOnIsInPipStateChangedListener(callback); + }); + } + + @Override + public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) { + mMainExecutor.execute(() -> { + mPipBoundsState.addPipExclusionBoundsChangeCallback(listener); + }); + } + + @Override + public void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { + mMainExecutor.execute(() -> { + mPipBoundsState.removePipExclusionBoundsChangeCallback(listener); + }); + } + + @Override + public void showPictureInPictureMenu() {} + + @Override + public void registerPipTransitionCallback( + PipTransitionController.PipTransitionCallback callback, + Executor executor) {} + } + /** * 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 e277a8dbeb13..ea02de9d9704 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 @@ -816,6 +816,26 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); mSpringingToTouch = false; mDismissalPending = false; + + // Check whether new bounds after fling imply we need to update stash state too. + stashEndActionIfNeeded(); + } + + private void stashEndActionIfNeeded() { + boolean isStashing = mPipBoundsState.getBounds().right > mPipBoundsState + .getDisplayBounds().width() || mPipBoundsState.getBounds().left < 0; + if (!isStashing) { + return; + } + + if (mPipBoundsState.getBounds().left < 0 + && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT) { + mPipBoundsState.setStashed(STASH_TYPE_LEFT); + } else if (mPipBoundsState.getBounds().left >= 0 + && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) { + mPipBoundsState.setStashed(STASH_TYPE_RIGHT); + } + mMenuController.hideMenu(); } /** 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 0c4ed268fd28..53b80e8b7542 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 @@ -106,9 +106,6 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha private float mStashVelocityThreshold; - // The reference inset bounds, used to determine the dismiss fraction - private final Rect mInsetBounds = new Rect(); - // Used to workaround an issue where the WM rotation happens before we are notified, allowing // us to send stale bounds private int mDeferResizeToNormalBoundsUntilRotation = -1; @@ -206,17 +203,10 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mMotionHelper, mainExecutor); mTouchState = new PipTouchState(ViewConfiguration.get(context), () -> { - if (mPipBoundsState.isStashed()) { - animateToUnStashedState(); - mPipUiEventLogger.log( - PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); - mPipBoundsState.setStashed(STASH_TYPE_NONE); - } else { - mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, - mPipBoundsState.getBounds(), true /* allowMenuTimeout */, - willResizeMenu(), - shouldShowResizeHandle()); - } + mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, + mPipBoundsState.getBounds(), true /* allowMenuTimeout */, + willResizeMenu(), + shouldShowResizeHandle()); }, menuController::hideMenu, mainExecutor); @@ -438,7 +428,6 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mPipBoundsState.setNormalMovementBounds(normalMovementBounds); mPipBoundsState.setExpandedMovementBounds(expandedMovementBounds); mDisplayRotation = displayRotation; - mInsetBounds.set(insetBounds); updateMovementBounds(); mMovementBoundsExtraOffsets = extraOffset; @@ -748,10 +737,13 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha final Rect pipBounds = mPipBoundsState.getBounds(); final boolean onLeftEdge = pipBounds.left < mPipBoundsState.getDisplayBounds().left; final Rect unStashedBounds = new Rect(0, pipBounds.top, 0, pipBounds.bottom); - unStashedBounds.left = onLeftEdge ? mInsetBounds.left - : mInsetBounds.right - pipBounds.width(); - unStashedBounds.right = onLeftEdge ? mInsetBounds.left + pipBounds.width() - : mInsetBounds.right; + + Rect insetBounds = new Rect(); + mPipBoundsAlgorithm.getInsetBounds(insetBounds); + unStashedBounds.left = onLeftEdge ? insetBounds.left + : insetBounds.right - pipBounds.width(); + unStashedBounds.right = onLeftEdge ? insetBounds.left + pipBounds.width() + : insetBounds.right; mMotionHelper.animateToUnStashedBounds(unStashedBounds); } @@ -899,8 +891,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha // Reset the touch state on up before the fling settles mTouchState.reset(); if (mEnableStash && shouldStash(vel, getPossiblyMotionBounds())) { - // mMotionHelper.stashToEdge(vel.x, vel.y, - // this::stashEndAction /* endAction */); + mMotionHelper.stashToEdge(vel.x, vel.y, null /* endAction */); } else { if (mPipBoundsState.isStashed()) { // Reset stashed state if previously stashed @@ -1030,8 +1021,10 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha * resized. */ private void updateMovementBounds() { + Rect insetBounds = new Rect(); + mPipBoundsAlgorithm.getInsetBounds(insetBounds); mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(), - mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0); + insetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0); mMotionHelper.onMovementBoundsChanged(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index a126cbe41b00..9750d3ec99f5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -535,7 +535,8 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { WindowContainerTransaction wct = new WindowContainerTransaction(); if (mCaptionInsets != null) { wct.addInsetsSource(mTaskToken, mCaptionInsetsOwner, 0, - WindowInsets.Type.captionBar(), mCaptionInsets, null /* boundingRects */); + WindowInsets.Type.captionBar(), mCaptionInsets, null /* boundingRects */, + 0 /* flags */); } else { wct.removeInsetsSource(mTaskToken, mCaptionInsetsOwner, 0, WindowInsets.Type.captionBar()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 73b32a24246a..778478405dda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -19,6 +19,7 @@ package com.android.wm.shell.transition; import static android.app.ActivityOptions.ANIM_CLIP_REVEAL; import static android.app.ActivityOptions.ANIM_CUSTOM; import static android.app.ActivityOptions.ANIM_NONE; +import static android.app.ActivityOptions.ANIM_FROM_STYLE; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.ActivityOptions.ANIM_SCALE_UP; import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION; @@ -40,6 +41,7 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECI import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; import static android.view.WindowManager.TRANSIT_RELAUNCH; +import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; @@ -62,6 +64,7 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo; +import static com.android.wm.shell.transition.TransitionAnimationHelper.isCoveredByOpaqueFullscreenChange; import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; import android.animation.Animator; @@ -352,6 +355,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { continue; } final boolean isTask = change.getTaskInfo() != null; + final boolean isFreeform = isTask && change.getTaskInfo().isFreeform(); final int mode = change.getMode(); boolean isSeamlessDisplayChange = false; @@ -458,6 +462,16 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final int layer = zSplitLine + numChanges - i; startTransaction.setLayer(change.getLeash(), layer); } + } else if (!isCoveredByOpaqueFullscreenChange(info, change) + && isFreeform + && TransitionUtil.isOpeningMode(type) + && change.getMode() == TRANSIT_TO_BACK) { + // Reparent the minimize-change to the root task so the minimizing Task + // isn't shown in front of other Tasks. + mRootTDAOrganizer.reparentToDisplayArea( + change.getTaskInfo().displayId, + change.getLeash(), + startTransaction); } else if (isOnlyTranslucent && TransitionUtil.isOpeningType(info.getType()) && TransitionUtil.isClosingType(mode)) { // If there is a closing translucent task in an OPENING transition, we will @@ -985,7 +999,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final int animType = options.getType(); return animType == ANIM_CUSTOM || animType == ANIM_SCALE_UP || animType == ANIM_THUMBNAIL_SCALE_UP || animType == ANIM_THUMBNAIL_SCALE_DOWN - || animType == ANIM_CLIP_REVEAL || animType == ANIM_OPEN_CROSS_PROFILE_APPS; + || animType == ANIM_CLIP_REVEAL || animType == ANIM_OPEN_CROSS_PROFILE_APPS + || animType == ANIM_FROM_STYLE; } private static void applyTransformation(long time, SurfaceControl.Transaction t, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index a5f071af6973..75e7ddf53f9f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -36,6 +36,7 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.WindowConfiguration; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; @@ -72,6 +73,9 @@ public class TransitionAnimationHelper { final int changeFlags = change.getFlags(); final boolean enter = TransitionUtil.isOpeningType(changeMode); final boolean isTask = change.getTaskInfo() != null; + final boolean isFreeform = isTask && change.getTaskInfo().isFreeform(); + final boolean isCoveredByOpaqueFullscreenChange = + isCoveredByOpaqueFullscreenChange(info, change); final TransitionInfo.AnimationOptions options; if (Flags.moveAnimationOptionsToChange()) { options = change.getAnimationOptions(); @@ -107,6 +111,24 @@ public class TransitionAnimationHelper { animAttr = enter ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation : R.styleable.WindowAnimation_wallpaperCloseExitAnimation; + } else if (!isCoveredByOpaqueFullscreenChange + && isFreeform + && TransitionUtil.isOpeningMode(type) + && change.getMode() == TRANSIT_TO_BACK) { + // Set translucent here so TransitionAnimation loads the appropriate animations for + // translucent activities and tasks later + translucent = (changeFlags & FLAG_TRANSLUCENT) != 0; + // The main Task is launching or being brought to front, this Task is being minimized + animAttr = R.styleable.WindowAnimation_activityCloseExitAnimation; + } else if (!isCoveredByOpaqueFullscreenChange + && isFreeform + && type == TRANSIT_TO_FRONT + && change.getMode() == TRANSIT_TO_FRONT) { + // Set translucent here so TransitionAnimation loads the appropriate animations for + // translucent activities and tasks later + translucent = (changeFlags & FLAG_TRANSLUCENT) != 0; + // Bring the minimized Task back to front + animAttr = R.styleable.WindowAnimation_activityOpenEnterAnimation; } else if (type == TRANSIT_OPEN) { // We will translucent open animation for translucent activities and tasks. Choose // WindowAnimation_activityOpenEnterAnimation and set translucent here, then @@ -417,4 +439,25 @@ public class TransitionAnimationHelper { return edgeExtensionLayer; } + + /** + * Returns whether there is an opaque fullscreen Change positioned in front of the given Change + * in the given TransitionInfo. + */ + static boolean isCoveredByOpaqueFullscreenChange( + TransitionInfo info, TransitionInfo.Change change) { + // TransitionInfo#getChanges() are ordered from front to back + for (TransitionInfo.Change coveringChange : info.getChanges()) { + if (coveringChange == change) { + return false; + } + if ((coveringChange.getFlags() & FLAG_TRANSLUCENT) == 0 + && coveringChange.getTaskInfo() != null + && coveringChange.getTaskInfo().getWindowingMode() + == WindowConfiguration.WINDOWING_MODE_FULLSCREEN) { + return true; + } + } + return false; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 21307a2edae0..8d53bebba7bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -31,12 +31,14 @@ import static android.view.WindowManager.fixScale; import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; +import static com.android.window.flags.Flags.enforceShellThreadModel; import static com.android.window.flags.Flags.ensureWallpaperInTransitions; import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; @@ -806,14 +808,16 @@ public class Transitions implements RemoteCallable<Transitions>, final int changeSize = info.getChanges().size(); boolean taskChange = false; boolean transferStartingWindow = false; - int noAnimationBehindStartingWindow = 0; + int animBehindStartingWindow = 0; boolean allOccluded = changeSize > 0; for (int i = changeSize - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); taskChange |= change.getTaskInfo() != null; transferStartingWindow |= change.hasFlags(FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT); - if (change.hasAllFlags(FLAG_IS_BEHIND_STARTING_WINDOW | FLAG_NO_ANIMATION)) { - noAnimationBehindStartingWindow++; + if (change.hasAllFlags(FLAG_IS_BEHIND_STARTING_WINDOW | FLAG_NO_ANIMATION) + || change.hasAllFlags( + FLAG_IS_BEHIND_STARTING_WINDOW | FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) { + animBehindStartingWindow++; } if (!change.hasFlags(FLAG_IS_OCCLUDED)) { allOccluded = false; @@ -831,11 +835,11 @@ public class Transitions implements RemoteCallable<Transitions>, // There does not need animation when: // A. Transfer starting window. Apply transfer starting window directly if there is no other // task change. Since this is an activity->activity situation, we can detect it by selecting - // transitions with only 2 changes where - // 1. neither are tasks, and + // transitions with changes where + // 1. none are tasks, and // 2. one is a starting-window recipient, or all change is behind starting window. - if (!taskChange && (transferStartingWindow || noAnimationBehindStartingWindow == changeSize) - && changeSize == 2 + if (!taskChange && (transferStartingWindow || animBehindStartingWindow == changeSize) + && changeSize >= 1 // B. It's visibility change if the TRANSIT_TO_BACK/TO_FRONT happened when all // changes are underneath another change. || ((info.getType() == TRANSIT_TO_BACK || info.getType() == TRANSIT_TO_FRONT) @@ -921,9 +925,12 @@ public class Transitions implements RemoteCallable<Transitions>, } // An existing animation is playing, so see if we can merge. final ActiveTransition playing = track.mActiveTransition; + final IBinder playingToken = playing.mToken; + final IBinder readyToken = ready.mToken; + if (ready.mAborted) { // record as merged since it is no-op. Calls back into processReadyQueue - onMerged(playing, ready); + onMerged(playingToken, readyToken); return; } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s ready while" @@ -931,14 +938,31 @@ public class Transitions implements RemoteCallable<Transitions>, + " in case they can be merged", ready, playing); mTransitionTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId()); playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, - playing.mToken, (wct) -> onMerged(playing, ready)); + playing.mToken, (wct) -> onMerged(playingToken, readyToken)); } - private void onMerged(@NonNull ActiveTransition playing, @NonNull ActiveTransition merged) { + private void onMerged(@NonNull IBinder playingToken, @NonNull IBinder mergedToken) { + if (enforceShellThreadModel()) { + mMainExecutor.assertCurrentThread(); + } + + ActiveTransition playing = mKnownTransitions.get(playingToken); + if (playing == null) { + Log.e(TAG, "Merging into a non-existent transition: " + playingToken); + return; + } + + ActiveTransition merged = mKnownTransitions.get(mergedToken); + if (merged == null) { + Log.e(TAG, "Merging a non-existent transition: " + mergedToken); + return; + } + if (playing.getTrack() != merged.getTrack()) { throw new IllegalStateException("Can't merge across tracks: " + merged + " into " + playing); } + final Track track = mTracks.get(playing.getTrack()); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s into %s", merged, playing); @@ -1068,13 +1092,17 @@ public class Transitions implements RemoteCallable<Transitions>, info.releaseAnimSurfaces(); } - private void onFinish(IBinder token, - @Nullable WindowContainerTransaction wct) { + private void onFinish(IBinder token, @Nullable WindowContainerTransaction wct) { + if (enforceShellThreadModel()) { + mMainExecutor.assertCurrentThread(); + } + final ActiveTransition active = mKnownTransitions.get(token); if (active == null) { Log.e(TAG, "Trying to finish a non-existent transition: " + token); return; } + final Track track = mTracks.get(active.getTrack()); if (track == null || track.mActiveTransition != active) { Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 95e0d79c212e..b9cb6d3d5007 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -26,12 +26,20 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import android.app.ActivityManager.RunningTaskInfo; import android.content.ContentResolver; import android.content.Context; +import android.graphics.Color; +import android.graphics.Point; import android.graphics.Rect; +import android.graphics.Region; +import android.hardware.input.InputManager; import android.os.Handler; +import android.os.RemoteException; import android.provider.Settings; +import android.util.Log; import android.util.SparseArray; import android.view.Choreographer; import android.view.Display; +import android.view.ISystemGestureExclusionListener; +import android.view.IWindowManager; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; @@ -45,39 +53,74 @@ import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.extension.TaskInfoKt; /** * View model for the window decoration with a caption and shadows. Works with * {@link CaptionWindowDecoration}. */ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { + private static final String TAG = "CaptionWindowDecorViewModel"; + private final ShellTaskOrganizer mTaskOrganizer; + private final IWindowManager mWindowManager; private final Context mContext; private final Handler mMainHandler; + private final ShellExecutor mMainExecutor; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final SyncTransactionQueue mSyncQueue; private final Transitions mTransitions; + private final Region mExclusionRegion = Region.obtain(); + private final InputManager mInputManager; private TaskOperations mTaskOperations; + /** + * Whether to pilfer the next motion event to send cancellations to the windows below. + * Useful when the caption window is spy and the gesture should be handled by the system + * instead of by the app for their custom header content. + */ + private boolean mShouldPilferCaptionEvents; + private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); + private final ISystemGestureExclusionListener mGestureExclusionListener = + new ISystemGestureExclusionListener.Stub() { + @Override + public void onSystemGestureExclusionChanged(int displayId, + Region systemGestureExclusion, Region systemGestureExclusionUnrestricted) { + if (mContext.getDisplayId() != displayId) { + return; + } + mMainExecutor.execute(() -> { + mExclusionRegion.set(systemGestureExclusion); + }); + } + }; + public CaptionWindowDecorViewModel( Context context, Handler mainHandler, + ShellExecutor shellExecutor, Choreographer mainChoreographer, + IWindowManager windowManager, + ShellInit shellInit, ShellTaskOrganizer taskOrganizer, DisplayController displayController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SyncTransactionQueue syncQueue, Transitions transitions) { mContext = context; + mMainExecutor = shellExecutor; mMainHandler = mainHandler; + mWindowManager = windowManager; mMainChoreographer = mainChoreographer; mTaskOrganizer = taskOrganizer; mDisplayController = displayController; @@ -87,6 +130,18 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (!Transitions.ENABLE_SHELL_TRANSITIONS) { mTaskOperations = new TaskOperations(null, mContext, mSyncQueue); } + mInputManager = mContext.getSystemService(InputManager.class); + + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + try { + mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener, + mContext.getDisplayId()); + } catch (RemoteException e) { + Log.e(TAG, "Failed to register window manager callbacks", e); + } } @Override @@ -178,8 +233,12 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { - final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); - decoration.setCaptionColor(statusBarColor); + if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { + decoration.setCaptionColor(Color.TRANSPARENT); + } else { + final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); + decoration.setCaptionColor(statusBarColor); + } } private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { @@ -301,6 +360,49 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue.queue(wct); } } + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + + final int actionMasked = e.getActionMasked(); + final boolean isDown = actionMasked == MotionEvent.ACTION_DOWN; + final boolean isUpOrCancel = actionMasked == MotionEvent.ACTION_CANCEL + || actionMasked == MotionEvent.ACTION_UP; + if (isDown) { + final boolean downInCustomizableCaptionRegion = + decoration.checkTouchEventInCustomizableRegion(e); + final boolean downInExclusionRegion = mExclusionRegion.contains( + (int) e.getRawX(), (int) e.getRawY()); + final boolean isTransparentCaption = + TaskInfoKt.isTransparentCaptionBarAppearance(decoration.mTaskInfo); + // MotionEvent's coordinates are relative to view, we want location in window + // to offset position relative to caption as a whole. + int[] viewLocation = new int[2]; + v.getLocationInWindow(viewLocation); + final boolean isResizeEvent = decoration.shouldResizeListenerHandleEvent(e, + new Point(viewLocation[0], viewLocation[1])); + // The caption window may be a spy window when the caption background is + // transparent, which means events will fall through to the app window. Make + // sure to cancel these events if they do not happen in the intersection of the + // customizable region and what the app reported as exclusion areas, because + // the drag-move or other caption gestures should take priority outside those + // regions. + mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion + && downInExclusionRegion && isTransparentCaption) && !isResizeEvent; + } + + if (!mShouldPilferCaptionEvents) { + // The event will be handled by a window below or pilfered by resize handler. + return false; + } + // Otherwise pilfer so that windows below receive cancellations for this gesture, and + // continue normal handling as a caption gesture. + if (mInputManager != null) { + // TODO(b/352127475): Only pilfer once per gesture + mInputManager.pilferPointers(v.getViewRootImpl().getInputToken()); + } + if (isUpOrCancel) { + // Gesture is finished, reset state. + mShouldPilferCaptionEvents = false; + } return mDragDetector.onMotionEvent(e); } 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 d0ca5b0fdce6..7e1b973a98f4 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 @@ -21,6 +21,7 @@ import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLarge import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import android.annotation.NonNull; +import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; @@ -28,22 +29,27 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.util.Size; import android.view.Choreographer; +import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; import android.view.ViewConfiguration; +import android.view.WindowManager; import android.window.WindowContainerTransaction; +import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.windowdecor.extension.TaskInfoKt; /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with @@ -177,12 +183,44 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL shouldSetTaskPositionAndCrop); } + @VisibleForTesting + static void updateRelayoutParams( + RelayoutParams relayoutParams, + ActivityManager.RunningTaskInfo taskInfo, + boolean applyStartTransactionOnDraw, + boolean setTaskCropAndPosition) { + relayoutParams.reset(); + relayoutParams.mRunningTaskInfo = taskInfo; + relayoutParams.mLayoutResId = R.layout.caption_window_decor; + relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); + relayoutParams.mShadowRadiusId = taskInfo.isFocused + ? R.dimen.freeform_decor_shadow_focused_thickness + : R.dimen.freeform_decor_shadow_unfocused_thickness; + relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; + relayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; + + if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { + // If the app is requesting to customize the caption bar, allow input to fall + // through to the windows below so that the app can respond to input events on + // their custom content. + relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; + } + final RelayoutParams.OccludingCaptionElement backButtonElement = + new RelayoutParams.OccludingCaptionElement(); + backButtonElement.mWidthResId = R.dimen.caption_left_buttons_width; + backButtonElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.START; + relayoutParams.mOccludingCaptionElements.add(backButtonElement); + // Then, the right-aligned section (minimize, maximize and close buttons). + final RelayoutParams.OccludingCaptionElement controlsElement = + new RelayoutParams.OccludingCaptionElement(); + controlsElement.mWidthResId = R.dimen.caption_right_buttons_width; + controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END; + relayoutParams.mOccludingCaptionElements.add(controlsElement); + } + void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) { - final int shadowRadiusID = taskInfo.isFocused - ? R.dimen.freeform_decor_shadow_focused_thickness - : R.dimen.freeform_decor_shadow_unfocused_thickness; final boolean isFreeform = taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; @@ -191,13 +229,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - mRelayoutParams.reset(); - mRelayoutParams.mRunningTaskInfo = taskInfo; - mRelayoutParams.mLayoutResId = R.layout.caption_window_decor; - mRelayoutParams.mCaptionHeightId = getCaptionHeightId(taskInfo.getWindowingMode()); - mRelayoutParams.mShadowRadiusId = shadowRadiusID; - mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; - mRelayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; + updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw, + setTaskCropAndPosition); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo @@ -303,6 +336,17 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mDragResizeListener = null; } + /** + * Checks whether the touch event falls inside the customizable caption region. + */ + boolean checkTouchEventInCustomizableRegion(MotionEvent ev) { + return mResult.mCustomizableCaptionRegion.contains((int) ev.getRawX(), (int) ev.getRawY()); + } + + boolean shouldResizeListenerHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { + return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset); + } + @Override public void close() { closeDragResizeListener(); @@ -311,6 +355,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL @Override int getCaptionHeightId(@WindowingMode int windowingMode) { + return getCaptionHeightIdStatic(windowingMode); + } + + private static int getCaptionHeightIdStatic(@WindowingMode int windowingMode) { return R.dimen.freeform_decor_caption_height; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index faf6a627febe..d12b9060bee3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -1040,7 +1040,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private void createInputChannel(int displayId) { final InputManager inputManager = mContext.getSystemService(InputManager.class); final InputMonitor inputMonitor = - mInputMonitorFactory.create(inputManager, mContext); + mInputMonitorFactory.create(inputManager, displayId); final EventReceiver eventReceiver = new EventReceiver(inputMonitor, inputMonitor.getInputChannel(), Looper.myLooper()); mEventReceiversByDisplay.put(displayId, eventReceiver); @@ -1194,8 +1194,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } static class InputMonitorFactory { - InputMonitor create(InputManager inputManager, Context context) { - return inputManager.monitorGestureInput("caption-touch", context.getDisplayId()); + InputMonitor create(InputManager inputManager, int displayId) { + return inputManager.monitorGestureInput("caption-touch", displayId); } } 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 5ffd883a7ceb..5d662b20ebb9 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 @@ -852,18 +852,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void createHandleMenu(SplitScreenController splitScreenController) { loadAppInfoIfNeeded(); - mHandleMenu = new HandleMenu.Builder(this) - .setAppIcon(mAppIconBitmap) - .setAppName(mAppName) - .setOnClickListener(mOnCaptionButtonClickListener) - .setOnTouchListener(mOnCaptionTouchListener) - .setLayoutId(mRelayoutParams.mLayoutResId) - .setWindowingButtonsVisible(DesktopModeStatus.canEnterDesktopMode(mContext)) - .setCaptionHeight(mResult.mCaptionHeight) - .setDisplayController(mDisplayController) - .setSplitScreenController(splitScreenController) - .setBrowserLinkAvailable(browserLinkAvailable()) - .build(); + mHandleMenu = new HandleMenu( + this, + mRelayoutParams.mLayoutResId, + mOnCaptionButtonClickListener, + mOnCaptionTouchListener, + mAppIconBitmap, + mAppName, + mDisplayController, + splitScreenController, + DesktopModeStatus.canEnterDesktopMode(mContext), + browserLinkAvailable(), + mResult.mCaptionHeight + ); mWindowDecorViewHolder.onHandleMenuOpened(); mHandleMenu.show(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java deleted file mode 100644 index 7e44f32bcbeb..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java +++ /dev/null @@ -1,539 +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.wm.shell.windowdecor; - -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.view.MotionEvent.ACTION_DOWN; -import static android.view.MotionEvent.ACTION_UP; - -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.ActivityManager.RunningTaskInfo; -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Point; -import android.graphics.PointF; -import android.graphics.Rect; -import android.view.MotionEvent; -import android.view.SurfaceControl; -import android.view.View; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; -import android.window.SurfaceSyncGroup; - -import androidx.annotation.VisibleForTesting; - -import com.android.window.flags.Flags; -import com.android.wm.shell.R; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer; - -/** - * Handle menu opened when the appropriate button is clicked on. - * - * Displays up to 3 pills that show the following: - * App Info: App name, app icon, and collapse button to close the menu. - * Windowing Options(Proto 2 only): Buttons to change windowing modes. - * Additional Options: Miscellaneous functions including screenshot and closing task. - */ -class HandleMenu { - private static final String TAG = "HandleMenu"; - private static final boolean SHOULD_SHOW_MORE_ACTIONS_PILL = false; - private final Context mContext; - private final DesktopModeWindowDecoration mParentDecor; - @VisibleForTesting - AdditionalViewContainer mHandleMenuViewContainer; - // Position of the handle menu used for laying out the handle view. - @VisibleForTesting - final PointF mHandleMenuPosition = new PointF(); - // With the introduction of {@link AdditionalSystemViewContainer}, {@link mHandleMenuPosition} - // may be in a different coordinate space than the input coordinates. Therefore, we still care - // about the menu's coordinates relative to the display as a whole, so we need to maintain - // those as well. - final Point mGlobalMenuPosition = new Point(); - private final boolean mShouldShowWindowingPill; - private final boolean mShouldShowBrowserPill; - private final Bitmap mAppIconBitmap; - private final CharSequence mAppName; - private final View.OnClickListener mOnClickListener; - private final View.OnTouchListener mOnTouchListener; - private final RunningTaskInfo mTaskInfo; - private final DisplayController mDisplayController; - private final SplitScreenController mSplitScreenController; - private final int mLayoutResId; - private int mMarginMenuTop; - private int mMarginMenuStart; - private int mMenuHeight; - private int mMenuWidth; - private final int mCaptionHeight; - private HandleMenuAnimator mHandleMenuAnimator; - - - HandleMenu(DesktopModeWindowDecoration parentDecor, int layoutResId, - View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, - Bitmap appIcon, CharSequence appName, DisplayController displayController, - SplitScreenController splitScreenController, boolean shouldShowWindowingPill, - boolean shouldShowBrowserPill, int captionHeight) { - mParentDecor = parentDecor; - mContext = mParentDecor.mDecorWindowContext; - mTaskInfo = mParentDecor.mTaskInfo; - mDisplayController = displayController; - mSplitScreenController = splitScreenController; - mLayoutResId = layoutResId; - mOnClickListener = onClickListener; - mOnTouchListener = onTouchListener; - mAppIconBitmap = appIcon; - mAppName = appName; - mShouldShowWindowingPill = shouldShowWindowingPill; - mShouldShowBrowserPill = shouldShowBrowserPill; - mCaptionHeight = captionHeight; - loadHandleMenuDimensions(); - updateHandleMenuPillPositions(); - } - - void show() { - final SurfaceSyncGroup ssg = new SurfaceSyncGroup(TAG); - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - - createHandleMenuViewContainer(t, ssg); - ssg.addTransaction(t); - ssg.markSyncReady(); - setupHandleMenu(); - animateHandleMenu(); - } - - private void createHandleMenuViewContainer(SurfaceControl.Transaction t, - SurfaceSyncGroup ssg) { - final int x = (int) mHandleMenuPosition.x; - final int y = (int) mHandleMenuPosition.y; - if (!mTaskInfo.isFreeform() && Flags.enableAdditionalWindowsAboveStatusBar()) { - mHandleMenuViewContainer = new AdditionalSystemViewContainer(mContext, - R.layout.desktop_mode_window_decor_handle_menu, mTaskInfo.taskId, - x, y, mMenuWidth, mMenuHeight); - } else { - mHandleMenuViewContainer = mParentDecor.addWindow( - R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu", - t, ssg, x, y, mMenuWidth, mMenuHeight); - } - final View handleMenuView = mHandleMenuViewContainer.getView(); - mHandleMenuAnimator = new HandleMenuAnimator(handleMenuView, mMenuWidth, mCaptionHeight); - } - - /** - * Animates the appearance of the handle menu and its three pills. - */ - private void animateHandleMenu() { - if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN - || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { - mHandleMenuAnimator.animateCaptionHandleExpandToOpen(); - } else { - mHandleMenuAnimator.animateOpen(); - } - } - - /** - * Set up all three pills of the handle menu: app info pill, windowing pill, & more actions - * pill. - */ - private void setupHandleMenu() { - final View handleMenu = mHandleMenuViewContainer.getView(); - handleMenu.setOnTouchListener(mOnTouchListener); - setupAppInfoPill(handleMenu); - if (mShouldShowWindowingPill) { - setupWindowingPill(handleMenu); - } - setupMoreActionsPill(handleMenu); - setupOpenInBrowserPill(handleMenu); - } - - /** - * Set up interactive elements of handle menu's app info pill. - */ - private void setupAppInfoPill(View handleMenu) { - final HandleMenuImageButton collapseBtn = - handleMenu.findViewById(R.id.collapse_menu_button); - final ImageView appIcon = handleMenu.findViewById(R.id.application_icon); - final TextView appName = handleMenu.findViewById(R.id.application_name); - collapseBtn.setOnClickListener(mOnClickListener); - collapseBtn.setTaskInfo(mTaskInfo); - appIcon.setImageBitmap(mAppIconBitmap); - appName.setText(mAppName); - } - - /** - * Set up interactive elements and color of handle menu's windowing pill. - */ - private void setupWindowingPill(View handleMenu) { - final ImageButton fullscreenBtn = handleMenu.findViewById( - R.id.fullscreen_button); - final ImageButton splitscreenBtn = handleMenu.findViewById( - R.id.split_screen_button); - final ImageButton floatingBtn = handleMenu.findViewById(R.id.floating_button); - // TODO: Remove once implemented. - floatingBtn.setVisibility(View.GONE); - - final ImageButton desktopBtn = handleMenu.findViewById(R.id.desktop_button); - fullscreenBtn.setOnClickListener(mOnClickListener); - splitscreenBtn.setOnClickListener(mOnClickListener); - floatingBtn.setOnClickListener(mOnClickListener); - desktopBtn.setOnClickListener(mOnClickListener); - // The button corresponding to the windowing mode that the task is currently in uses a - // different color than the others. - final ColorStateList[] iconColors = getWindowingIconColor(); - final ColorStateList inActiveColorStateList = iconColors[0]; - final ColorStateList activeColorStateList = iconColors[1]; - final int windowingMode = mTaskInfo.getWindowingMode(); - fullscreenBtn.setImageTintList(windowingMode == WINDOWING_MODE_FULLSCREEN - ? activeColorStateList : inActiveColorStateList); - splitscreenBtn.setImageTintList(windowingMode == WINDOWING_MODE_MULTI_WINDOW - ? activeColorStateList : inActiveColorStateList); - floatingBtn.setImageTintList(windowingMode == WINDOWING_MODE_PINNED - ? activeColorStateList : inActiveColorStateList); - desktopBtn.setImageTintList(windowingMode == WINDOWING_MODE_FREEFORM - ? activeColorStateList : inActiveColorStateList); - } - - /** - * Set up interactive elements & height of handle menu's more actions pill - */ - private void setupMoreActionsPill(View handleMenu) { - if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { - handleMenu.findViewById(R.id.more_actions_pill).setVisibility(View.GONE); - } - } - - private void setupOpenInBrowserPill(View handleMenu) { - if (!mShouldShowBrowserPill) { - handleMenu.findViewById(R.id.open_in_browser_pill).setVisibility(View.GONE); - return; - } - final Button browserButton = handleMenu.findViewById(R.id.open_in_browser_button); - browserButton.setOnClickListener(mOnClickListener); - } - - /** - * Returns array of windowing icon color based on current UI theme. First element of the - * array is for inactive icons and the second is for active icons. - */ - private ColorStateList[] getWindowingIconColor() { - final int mode = mContext.getResources().getConfiguration().uiMode - & Configuration.UI_MODE_NIGHT_MASK; - final boolean isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES); - final TypedArray typedArray = mContext.obtainStyledAttributes(new int[]{ - com.android.internal.R.attr.materialColorOnSurface, - com.android.internal.R.attr.materialColorPrimary}); - final int inActiveColor = typedArray.getColor(0, isNightMode ? Color.WHITE : Color.BLACK); - final int activeColor = typedArray.getColor(1, isNightMode ? Color.WHITE : Color.BLACK); - typedArray.recycle(); - return new ColorStateList[]{ColorStateList.valueOf(inActiveColor), - ColorStateList.valueOf(activeColor)}; - } - - /** - * Updates handle menu's position variables to reflect its next position. - */ - private void updateHandleMenuPillPositions() { - int menuX; - final int menuY; - final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds(); - updateGlobalMenuPosition(taskBounds); - if (mLayoutResId == R.layout.desktop_mode_app_header) { - // Align the handle menu to the left side of the caption. - menuX = mMarginMenuStart; - menuY = mMarginMenuTop; - } else { - if (Flags.enableAdditionalWindowsAboveStatusBar()) { - // In a focused decor, we use global coordinates for handle menu. Therefore we - // need to account for other factors like split stage and menu/handle width to - // center the menu. - final DisplayLayout layout = mDisplayController - .getDisplayLayout(mTaskInfo.displayId); - menuX = mGlobalMenuPosition.x + ((mMenuWidth - layout.width()) / 2); - menuY = mGlobalMenuPosition.y + ((mMenuHeight - layout.height()) / 2); - } else { - menuX = (taskBounds.width() / 2) - (mMenuWidth / 2); - menuY = mMarginMenuTop; - } - } - // Handle Menu position setup. - mHandleMenuPosition.set(menuX, menuY); - } - - private void updateGlobalMenuPosition(Rect taskBounds) { - if (mTaskInfo.isFreeform()) { - mGlobalMenuPosition.set(taskBounds.left + mMarginMenuStart, - taskBounds.top + mMarginMenuTop); - } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { - mGlobalMenuPosition.set( - (taskBounds.width() / 2) - (mMenuWidth / 2) + mMarginMenuStart, - mMarginMenuTop - ); - } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { - final int splitPosition = mSplitScreenController.getSplitPosition(mTaskInfo.taskId); - final Rect leftOrTopStageBounds = new Rect(); - final Rect rightOrBottomStageBounds = new Rect(); - mSplitScreenController.getStageBounds(leftOrTopStageBounds, - rightOrBottomStageBounds); - // TODO(b/343561161): This needs to be calculated differently if the task is in - // top/bottom split. - if (splitPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) { - mGlobalMenuPosition.set(leftOrTopStageBounds.width() - + (rightOrBottomStageBounds.width() / 2) - - (mMenuWidth / 2) + mMarginMenuStart, - mMarginMenuTop); - } else if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { - mGlobalMenuPosition.set((leftOrTopStageBounds.width() / 2) - - (mMenuWidth / 2) + mMarginMenuStart, - mMarginMenuTop); - } - } - } - - /** - * Update pill layout, in case task changes have caused positioning to change. - */ - void relayout(SurfaceControl.Transaction t) { - if (mHandleMenuViewContainer != null) { - updateHandleMenuPillPositions(); - mHandleMenuViewContainer.setPosition(t, mHandleMenuPosition.x, mHandleMenuPosition.y); - } - } - - /** - * Check a passed MotionEvent if a click or hover has occurred on any button on this caption - * Note this should only be called when a regular onClick/onHover is not possible - * (i.e. the button was clicked through status bar layer) - * - * @param ev the MotionEvent to compare against. - */ - void checkMotionEvent(MotionEvent ev) { - // If the menu view is above status bar, we can let the views handle input directly. - if (isViewAboveStatusBar()) return; - final View handleMenu = mHandleMenuViewContainer.getView(); - final HandleMenuImageButton collapse = handleMenu.findViewById(R.id.collapse_menu_button); - final PointF inputPoint = translateInputToLocalSpace(ev); - final boolean inputInCollapseButton = pointInView(collapse, inputPoint.x, inputPoint.y); - final int action = ev.getActionMasked(); - collapse.setHovered(inputInCollapseButton && action != ACTION_UP); - collapse.setPressed(inputInCollapseButton && action == ACTION_DOWN); - if (action == ACTION_UP && inputInCollapseButton) { - collapse.performClick(); - } - } - - private boolean isViewAboveStatusBar() { - return Flags.enableAdditionalWindowsAboveStatusBar() - && !mTaskInfo.isFreeform(); - } - - // Translate the input point from display coordinates to the same space as the handle menu. - private PointF translateInputToLocalSpace(MotionEvent ev) { - return new PointF(ev.getX() - mHandleMenuPosition.x, - ev.getY() - mHandleMenuPosition.y); - } - - /** - * A valid menu input is one of the following: - * An input that happens in the menu views. - * Any input before the views have been laid out. - * - * @param inputPoint the input to compare against. - */ - boolean isValidMenuInput(PointF inputPoint) { - if (!viewsLaidOut()) return true; - if (!isViewAboveStatusBar()) { - return pointInView( - mHandleMenuViewContainer.getView(), - inputPoint.x - mHandleMenuPosition.x, - inputPoint.y - mHandleMenuPosition.y); - } else { - // Handle menu exists in a different coordinate space when added to WindowManager. - // Therefore we must compare the provided input coordinates to global menu coordinates. - // This includes factoring for split stage as input coordinates are relative to split - // stage position, not relative to the display as a whole. - PointF inputRelativeToMenu = new PointF( - inputPoint.x - mGlobalMenuPosition.x, - inputPoint.y - mGlobalMenuPosition.y - ); - if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId) - == SPLIT_POSITION_BOTTOM_OR_RIGHT) { - // TODO(b/343561161): This also needs to be calculated differently if - // the task is in top/bottom split. - Rect leftStageBounds = new Rect(); - mSplitScreenController.getStageBounds(leftStageBounds, new Rect()); - inputRelativeToMenu.x += leftStageBounds.width(); - } - return pointInView( - mHandleMenuViewContainer.getView(), - inputRelativeToMenu.x, - inputRelativeToMenu.y); - } - } - - private boolean pointInView(View v, float x, float y) { - return v != null && v.getLeft() <= x && v.getRight() >= x - && v.getTop() <= y && v.getBottom() >= y; - } - - /** - * Check if the views for handle menu can be seen. - */ - private boolean viewsLaidOut() { - return mHandleMenuViewContainer.getView().isLaidOut(); - } - - private void loadHandleMenuDimensions() { - final Resources resources = mContext.getResources(); - mMenuWidth = loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_width); - mMenuHeight = getHandleMenuHeight(resources); - mMarginMenuTop = loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_margin_top); - mMarginMenuStart = loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_margin_start); - } - - /** - * Determines handle menu height based on if windowing pill should be shown. - */ - private int getHandleMenuHeight(Resources resources) { - int menuHeight = loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_height); - if (!mShouldShowWindowingPill) { - menuHeight -= loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_windowing_pill_height); - } - if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { - menuHeight -= loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_more_actions_pill_height); - } - if (!mShouldShowBrowserPill) { - menuHeight -= loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_open_in_browser_pill_height); - } - return menuHeight; - } - - private int loadDimensionPixelSize(Resources resources, int resourceId) { - if (resourceId == Resources.ID_NULL) { - return 0; - } - return resources.getDimensionPixelSize(resourceId); - } - - void close() { - final Runnable after = () -> { - mHandleMenuViewContainer.releaseView(); - mHandleMenuViewContainer = null; - }; - if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN - || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { - mHandleMenuAnimator.animateCollapseIntoHandleClose(after); - } else { - mHandleMenuAnimator.animateClose(after); - } - } - - static final class Builder { - private final DesktopModeWindowDecoration mParent; - private CharSequence mName; - private Bitmap mAppIcon; - private View.OnClickListener mOnClickListener; - private View.OnTouchListener mOnTouchListener; - private int mLayoutId; - private boolean mShowWindowingPill; - private int mCaptionHeight; - private DisplayController mDisplayController; - private SplitScreenController mSplitScreenController; - private boolean mShowBrowserPill; - - Builder(@NonNull DesktopModeWindowDecoration parent) { - mParent = parent; - } - - Builder setAppName(@Nullable CharSequence name) { - mName = name; - return this; - } - - Builder setAppIcon(@Nullable Bitmap appIcon) { - mAppIcon = appIcon; - return this; - } - - Builder setOnClickListener(@Nullable View.OnClickListener onClickListener) { - mOnClickListener = onClickListener; - return this; - } - - Builder setOnTouchListener(@Nullable View.OnTouchListener onTouchListener) { - mOnTouchListener = onTouchListener; - return this; - } - - Builder setLayoutId(int layoutId) { - mLayoutId = layoutId; - return this; - } - - Builder setWindowingButtonsVisible(boolean windowingButtonsVisible) { - mShowWindowingPill = windowingButtonsVisible; - return this; - } - - Builder setCaptionHeight(int captionHeight) { - mCaptionHeight = captionHeight; - return this; - } - - Builder setDisplayController(DisplayController displayController) { - mDisplayController = displayController; - return this; - } - - Builder setSplitScreenController(SplitScreenController splitScreenController) { - mSplitScreenController = splitScreenController; - return this; - } - - Builder setBrowserLinkAvailable(Boolean showBrowserPill) { - mShowBrowserPill = showBrowserPill; - return this; - } - - HandleMenu build() { - return new HandleMenu(mParent, mLayoutId, mOnClickListener, - mOnTouchListener, mAppIcon, mName, mDisplayController, mSplitScreenController, - mShowWindowingPill, mShowBrowserPill, mCaptionHeight); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt new file mode 100644 index 000000000000..bce233fb0b52 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -0,0 +1,484 @@ +/* + * 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.wm.shell.windowdecor + +import android.annotation.DimenRes +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Point +import android.graphics.PointF +import android.graphics.Rect +import android.view.MotionEvent +import android.view.SurfaceControl +import android.view.View +import android.widget.Button +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import android.window.SurfaceSyncGroup +import androidx.annotation.VisibleForTesting +import com.android.window.flags.Flags +import com.android.wm.shell.R +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.split.SplitScreenConstants +import com.android.wm.shell.splitscreen.SplitScreenController +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer +import com.android.wm.shell.windowdecor.extension.isFullscreen + +/** + * Handle menu opened when the appropriate button is clicked on. + * + * Displays up to 3 pills that show the following: + * App Info: App name, app icon, and collapse button to close the menu. + * Windowing Options(Proto 2 only): Buttons to change windowing modes. + * Additional Options: Miscellaneous functions including screenshot and closing task. + */ +class HandleMenu( + private val parentDecor: DesktopModeWindowDecoration, + private val layoutResId: Int, + private val onClickListener: View.OnClickListener?, + private val onTouchListener: View.OnTouchListener?, + private val appIconBitmap: Bitmap?, + private val appName: CharSequence?, + private val displayController: DisplayController, + private val splitScreenController: SplitScreenController, + private val shouldShowWindowingPill: Boolean, + private val shouldShowBrowserPill: Boolean, + private val captionHeight: Int +) { + private val context: Context = parentDecor.mDecorWindowContext + private val taskInfo: ActivityManager.RunningTaskInfo = parentDecor.mTaskInfo + + private val isViewAboveStatusBar: Boolean + get() = (Flags.enableAdditionalWindowsAboveStatusBar() && !taskInfo.isFreeform) + + private val pillElevation: Int = loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_pill_elevation) + private val pillTopMargin: Int = loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_pill_spacing_margin) + private val menuWidth = loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_width) + pillElevation + private val menuHeight = getHandleMenuHeight() + private val marginMenuTop = loadDimensionPixelSize(R.dimen.desktop_mode_handle_menu_margin_top) + private val marginMenuStart = loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_margin_start) + + private var handleMenuAnimator: HandleMenuAnimator? = null + + @VisibleForTesting + var handleMenuViewContainer: AdditionalViewContainer? = null + + // Position of the handle menu used for laying out the handle view. + @VisibleForTesting + val handleMenuPosition: PointF = PointF() + + // With the introduction of {@link AdditionalSystemViewContainer}, {@link mHandleMenuPosition} + // may be in a different coordinate space than the input coordinates. Therefore, we still care + // about the menu's coordinates relative to the display as a whole, so we need to maintain + // those as well. + private val globalMenuPosition: Point = Point() + + /** + * An a array of windowing icon color based on current UI theme. First element of the + * array is for inactive icons and the second is for active icons. + */ + private val windowingIconColor: Array<ColorStateList> + get() { + val mode = (context.resources.configuration.uiMode + and Configuration.UI_MODE_NIGHT_MASK) + val isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES) + val typedArray = context.obtainStyledAttributes( + intArrayOf( + com.android.internal.R.attr.materialColorOnSurface, + com.android.internal.R.attr.materialColorPrimary + ) + ) + val inActiveColor = + typedArray.getColor(0, if (isNightMode) Color.WHITE else Color.BLACK) + val activeColor = typedArray.getColor(1, if (isNightMode) Color.WHITE else Color.BLACK) + typedArray.recycle() + return arrayOf( + ColorStateList.valueOf(inActiveColor), + ColorStateList.valueOf(activeColor) + ) + } + + init { + updateHandleMenuPillPositions() + } + + fun show() { + val ssg = SurfaceSyncGroup(TAG) + val t = SurfaceControl.Transaction() + + createHandleMenuViewContainer(t, ssg) + ssg.addTransaction(t) + ssg.markSyncReady() + setupHandleMenu() + animateHandleMenu() + } + + private fun createHandleMenuViewContainer( + t: SurfaceControl.Transaction, + ssg: SurfaceSyncGroup + ) { + val x = handleMenuPosition.x.toInt() + val y = handleMenuPosition.y.toInt() + handleMenuViewContainer = + if (!taskInfo.isFreeform && Flags.enableAdditionalWindowsAboveStatusBar()) { + AdditionalSystemViewContainer( + context = context, + layoutId = R.layout.desktop_mode_window_decor_handle_menu, + taskId = taskInfo.taskId, + x = x, + y = y, + width = menuWidth, + height = menuHeight + ) + } else { + parentDecor.addWindow( + R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu", + t, ssg, x, y, menuWidth, menuHeight + ) + } + handleMenuViewContainer?.view?.let { view -> + handleMenuAnimator = + HandleMenuAnimator(view, menuWidth, captionHeight.toFloat()) + } + } + + /** + * Animates the appearance of the handle menu and its three pills. + */ + private fun animateHandleMenu() { + when (taskInfo.windowingMode) { + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, + WINDOWING_MODE_MULTI_WINDOW -> { + handleMenuAnimator?.animateCaptionHandleExpandToOpen() + } + else -> { + handleMenuAnimator?.animateOpen() + } + } + } + + /** + * Set up all three pills of the handle menu: app info pill, windowing pill, & more actions + * pill. + */ + private fun setupHandleMenu() { + val handleMenu = handleMenuViewContainer?.view ?: return + handleMenu.setOnTouchListener(onTouchListener) + setupAppInfoPill(handleMenu) + if (shouldShowWindowingPill) { + setupWindowingPill(handleMenu) + } + setupMoreActionsPill(handleMenu) + setupOpenInBrowserPill(handleMenu) + } + + /** + * Set up interactive elements of handle menu's app info pill. + */ + private fun setupAppInfoPill(handleMenu: View) { + val collapseBtn = handleMenu.findViewById<HandleMenuImageButton>(R.id.collapse_menu_button) + val appIcon = handleMenu.findViewById<ImageView>(R.id.application_icon) + val appName = handleMenu.findViewById<TextView>(R.id.application_name) + collapseBtn.setOnClickListener(onClickListener) + collapseBtn.taskInfo = taskInfo + appIcon.setImageBitmap(appIconBitmap) + appName.text = this.appName + } + + /** + * Set up interactive elements and color of handle menu's windowing pill. + */ + private fun setupWindowingPill(handleMenu: View) { + val fullscreenBtn = handleMenu.findViewById<ImageButton>(R.id.fullscreen_button) + val splitscreenBtn = handleMenu.findViewById<ImageButton>(R.id.split_screen_button) + val floatingBtn = handleMenu.findViewById<ImageButton>(R.id.floating_button) + // TODO: Remove once implemented. + floatingBtn.visibility = View.GONE + + val desktopBtn = handleMenu.findViewById<ImageButton>(R.id.desktop_button) + fullscreenBtn.setOnClickListener(onClickListener) + splitscreenBtn.setOnClickListener(onClickListener) + floatingBtn.setOnClickListener(onClickListener) + desktopBtn.setOnClickListener(onClickListener) + // The button corresponding to the windowing mode that the task is currently in uses a + // different color than the others. + val iconColors = windowingIconColor + val inActiveColorStateList = iconColors[0] + val activeColorStateList = iconColors[1] + fullscreenBtn.imageTintList = if (taskInfo.isFullscreen) { + activeColorStateList + } else { + inActiveColorStateList + } + splitscreenBtn.imageTintList = if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + activeColorStateList + } else { + inActiveColorStateList + } + floatingBtn.imageTintList = if (taskInfo.windowingMode == WINDOWING_MODE_PINNED) { + activeColorStateList + } else { + inActiveColorStateList + } + desktopBtn.imageTintList = if (taskInfo.isFreeform) { + activeColorStateList + } else { + inActiveColorStateList + } + } + + /** + * Set up interactive elements & height of handle menu's more actions pill + */ + private fun setupMoreActionsPill(handleMenu: View) { + if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { + handleMenu.findViewById<View>(R.id.more_actions_pill).visibility = View.GONE + } + } + + private fun setupOpenInBrowserPill(handleMenu: View) { + if (!shouldShowBrowserPill) { + handleMenu.findViewById<View>(R.id.open_in_browser_pill).visibility = View.GONE + return + } + val browserButton = handleMenu.findViewById<Button>(R.id.open_in_browser_button) + browserButton.setOnClickListener(onClickListener) + } + + /** + * Updates handle menu's position variables to reflect its next position. + */ + private fun updateHandleMenuPillPositions() { + val menuX: Int + val menuY: Int + val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds + updateGlobalMenuPosition(taskBounds) + if (layoutResId == R.layout.desktop_mode_app_header) { + // Align the handle menu to the left side of the caption. + menuX = marginMenuStart + menuY = marginMenuTop + } else { + if (Flags.enableAdditionalWindowsAboveStatusBar()) { + // In a focused decor, we use global coordinates for handle menu. Therefore we + // need to account for other factors like split stage and menu/handle width to + // center the menu. + menuX = globalMenuPosition.x + menuY = globalMenuPosition.y + } else { + menuX = (taskBounds.width() / 2) - (menuWidth / 2) + menuY = marginMenuTop + } + } + // Handle Menu position setup. + handleMenuPosition.set(menuX.toFloat(), menuY.toFloat()) + } + + private fun updateGlobalMenuPosition(taskBounds: Rect) { + when (taskInfo.windowingMode) { + WINDOWING_MODE_FREEFORM -> { + globalMenuPosition.set( + /* x = */ taskBounds.left + marginMenuStart, + /* y = */ taskBounds.top + marginMenuTop + ) + } + WINDOWING_MODE_FULLSCREEN -> { + globalMenuPosition.set( + /* x = */ taskBounds.width() / 2 - (menuWidth / 2), + /* y = */ marginMenuTop + ) + } + WINDOWING_MODE_MULTI_WINDOW -> { + val splitPosition = splitScreenController.getSplitPosition(taskInfo.taskId) + val leftOrTopStageBounds = Rect() + val rightOrBottomStageBounds = Rect() + splitScreenController.getStageBounds(leftOrTopStageBounds, rightOrBottomStageBounds) + // TODO(b/343561161): This needs to be calculated differently if the task is in + // top/bottom split. + when (splitPosition) { + SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> { + globalMenuPosition.set( + /* x = */ leftOrTopStageBounds.width() + + (rightOrBottomStageBounds.width() / 2) + - (menuWidth / 2), + /* y = */ marginMenuTop + ) + } + SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> { + globalMenuPosition.set( + /* x = */ (leftOrTopStageBounds.width() / 2) + - (menuWidth / 2), + /* y = */ marginMenuTop + ) + } + } + } + } + } + + /** + * Update pill layout, in case task changes have caused positioning to change. + */ + fun relayout(t: SurfaceControl.Transaction) { + handleMenuViewContainer?.let { container -> + updateHandleMenuPillPositions() + container.setPosition(t, handleMenuPosition.x, handleMenuPosition.y) + } + } + + /** + * Check a passed MotionEvent if a click or hover has occurred on any button on this caption + * Note this should only be called when a regular onClick/onHover is not possible + * (i.e. the button was clicked through status bar layer) + * + * @param ev the MotionEvent to compare against. + */ + fun checkMotionEvent(ev: MotionEvent) { + // If the menu view is above status bar, we can let the views handle input directly. + if (isViewAboveStatusBar) return + val handleMenu = handleMenuViewContainer?.view ?: return + val collapse = handleMenu.findViewById<HandleMenuImageButton>(R.id.collapse_menu_button) + val inputPoint = translateInputToLocalSpace(ev) + val inputInCollapseButton = pointInView(collapse, inputPoint.x, inputPoint.y) + val action = ev.actionMasked + collapse.isHovered = inputInCollapseButton && action != MotionEvent.ACTION_UP + collapse.isPressed = inputInCollapseButton && action == MotionEvent.ACTION_DOWN + if (action == MotionEvent.ACTION_UP && inputInCollapseButton) { + collapse.performClick() + } + } + + // Translate the input point from display coordinates to the same space as the handle menu. + private fun translateInputToLocalSpace(ev: MotionEvent): PointF { + return PointF( + ev.x - handleMenuPosition.x, + ev.y - handleMenuPosition.y + ) + } + + /** + * A valid menu input is one of the following: + * An input that happens in the menu views. + * Any input before the views have been laid out. + * + * @param inputPoint the input to compare against. + */ + fun isValidMenuInput(inputPoint: PointF): Boolean { + if (!viewsLaidOut()) return true + if (!isViewAboveStatusBar) { + return pointInView( + handleMenuViewContainer?.view, + inputPoint.x - handleMenuPosition.x, + inputPoint.y - handleMenuPosition.y + ) + } else { + // Handle menu exists in a different coordinate space when added to WindowManager. + // Therefore we must compare the provided input coordinates to global menu coordinates. + // This includes factoring for split stage as input coordinates are relative to split + // stage position, not relative to the display as a whole. + val inputRelativeToMenu = PointF( + inputPoint.x - globalMenuPosition.x, + inputPoint.y - globalMenuPosition.y + ) + if (splitScreenController.getSplitPosition(taskInfo.taskId) + == SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT) { + // TODO(b/343561161): This also needs to be calculated differently if + // the task is in top/bottom split. + val leftStageBounds = Rect() + splitScreenController.getStageBounds(leftStageBounds, Rect()) + inputRelativeToMenu.x += leftStageBounds.width().toFloat() + } + return pointInView( + handleMenuViewContainer?.view, + inputRelativeToMenu.x, + inputRelativeToMenu.y + ) + } + } + + private fun pointInView(v: View?, x: Float, y: Float): Boolean { + return v != null && v.left <= x && v.right >= x && v.top <= y && v.bottom >= y + } + + /** + * Check if the views for handle menu can be seen. + */ + private fun viewsLaidOut(): Boolean = handleMenuViewContainer?.view?.isLaidOut ?: false + + /** + * Determines handle menu height based the max size and the visibility of pills. + */ + private fun getHandleMenuHeight(): Int { + var menuHeight = loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_height) + pillElevation + if (!shouldShowWindowingPill) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_windowing_pill_height) + menuHeight -= pillTopMargin + } + if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_more_actions_pill_height) + menuHeight -= pillTopMargin + } + if (!shouldShowBrowserPill) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_open_in_browser_pill_height) + menuHeight -= pillTopMargin + } + return menuHeight + } + + private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int { + if (resourceId == Resources.ID_NULL) { + return 0 + } + return context.resources.getDimensionPixelSize(resourceId) + } + + fun close() { + val after = { + handleMenuViewContainer?.releaseView() + handleMenuViewContainer = null + } + if (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN || + taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + handleMenuAnimator?.animateCollapseIntoHandleClose(after) + } else { + handleMenuAnimator?.animateClose(after) + } + } + + companion object { + private const val TAG = "HandleMenu" + private const val SHOULD_SHOW_MORE_ACTIONS_PILL = false + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt index 25a829b44448..e3d22342cc9b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt @@ -108,7 +108,7 @@ class HandleMenuAnimator( * * @param after runs after the animation finishes. */ - fun animateCollapseIntoHandleClose(after: Runnable) { + fun animateCollapseIntoHandleClose(after: () -> Unit) { appInfoCollapseToHandle() animateAppInfoPillFadeOut() windowingPillClose() @@ -125,7 +125,7 @@ class HandleMenuAnimator( * @param after runs after animation finishes. * */ - fun animateClose(after: Runnable) { + fun animateClose(after: () -> Unit) { appInfoPillCollapse() animateAppInfoPillFadeOut() windowingPillClose() @@ -463,9 +463,9 @@ class HandleMenuAnimator( * * @param after runs after animation finishes. */ - private fun runAnimations(after: Runnable? = null) { + private fun runAnimations(after: (() -> Unit)? = null) { runningAnimation?.apply { - // Remove all listeners, so that after runnable isn't triggered upon cancel. + // Remove all listeners, so that the after function isn't triggered upon cancel. removeAllListeners() // If an animation runs while running animation is triggered, gracefully cancel. cancel() @@ -475,7 +475,7 @@ class HandleMenuAnimator( playTogether(animators) animators.clear() doOnEnd { - after?.run() + after?.invoke() runningAnimation = null } start() 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 03dbbb3bbb82..d212f2131ed4 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 @@ -19,6 +19,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.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.mandatorySystemGestures; import static android.view.WindowInsets.Type.statusBars; @@ -53,6 +54,7 @@ import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; @@ -105,7 +107,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> * System-wide context. Only used to create context with overridden configurations. */ final Context mContext; - final DisplayController mDisplayController; + final @NonNull DisplayController mDisplayController; final ShellTaskOrganizer mTaskOrganizer; final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; @@ -158,7 +160,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> WindowDecoration( Context context, - DisplayController displayController, + @NonNull DisplayController displayController, ShellTaskOrganizer taskOrganizer, RunningTaskInfo taskInfo, @NonNull SurfaceControl taskSurface, @@ -759,9 +761,12 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } void addOrUpdate(WindowContainerTransaction wct) { - wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects); + final @InsetsSource.Flags int captionSourceFlags = + Flags.enableCaptionCompatInsetForceConsumption() ? FLAG_FORCE_CONSUMING : 0; + wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects, + captionSourceFlags); wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame, - mBoundingRects); + mBoundingRects, 0 /* flags */); } void remove(WindowContainerTransaction wct) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt index 6c2c8fd46bc9..4897f76a20cf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor.additionalviewcontainer import android.content.Context import android.graphics.PixelFormat +import android.view.Gravity import android.view.LayoutInflater import android.view.SurfaceControl import android.view.View @@ -45,9 +46,11 @@ class AdditionalSystemViewContainer( WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT - ) - lp.title = "Additional view container of Task=$taskId" - lp.setTrustedOverlay() + ).apply { + title = "Additional view container of Task=$taskId" + gravity = Gravity.LEFT or Gravity.TOP + setTrustedOverlay() + } val wm: WindowManager? = context.getSystemService(WindowManager::class.java) wm?.addView(view, lp) } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ExitDesktopWithDragToTopDragZone.kt new file mode 100644 index 000000000000..0b6c9af17e7a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ExitDesktopWithDragToTopDragZone.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.flicker.service.common.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + + +@Ignore("Base Test Class") +abstract class ExitDesktopWithDragToTopDragZone +@JvmOverloads +constructor(val rotation: Rotation = Rotation.ROTATION_0) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun exitDesktopWithDragToTopDragZone() { + testApp.exitDesktopWithDragToTopDragZone(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 6cabbf90bbc2..8558a77e4e7e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -311,6 +311,25 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { + val homeTask = setUpHomeTask(SECOND_DISPLAY) + val task1 = setUpFreeformTask(SECOND_DISPLAY) + val task2 = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 (no wallpaper intent) + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() @@ -330,6 +349,25 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() { + val homeTask = setUpHomeTask(SECOND_DISPLAY) + val task1 = setUpFreeformTask(SECOND_DISPLAY) + val task2 = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { val task1 = setUpFreeformTask() @@ -427,6 +465,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) setUpHomeTask(SECOND_DISPLAY) val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) @@ -436,10 +475,13 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(2) - // Expect order to be from bottom: wallpaper intent, task - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - wct.assertReorderAt(index = 1, taskDefaultDisplay) + assertThat(wct.hierarchyOps).hasSize(3) + // Move home to front + wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) + // Add desktop wallpaper activity + wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) + // Move freeform task to front + wct.assertReorderAt(index = 2, taskDefaultDisplay) } @Test @@ -464,7 +506,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { - setUpHomeTask() + val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val minimizedTask = setUpFreeformTask() @@ -474,11 +516,13 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(2) + assertThat(wct.hierarchyOps).hasSize(3) + // Move home to front + wct.assertReorderAt(index = 0, homeTask, toTop = true) // Add desktop wallpaper activity - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) // Reorder freeform task to top, don't reorder the minimized task - wct.assertReorderAt(index = 1, freeformTask, toTop = true) + wct.assertReorderAt(index = 2, freeformTask, toTop = true) } @Test @@ -861,16 +905,19 @@ class DesktopTasksControllerTest : ShellTestCase() { val taskLimit = desktopTasksLimiter.getMaxTaskLimit() val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() - setUpHomeTask() + val homeTask = setUpHomeTask() controller.moveToDesktop(newTask, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() - assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + wallpaper + assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 2) // tasks + home + wallpaper + // Move home to front + wct.assertReorderAt(0, homeTask) // Add desktop wallpaper activity - wct.assertPendingIntentAt(0, desktopWallpaperIntent) + wct.assertPendingIntentAt(1, desktopWallpaperIntent) + // Bring freeform tasks to front wct.assertReorderSequenceInRange( - range = 1..<(taskLimit + 1), + range = 2..<(taskLimit + 2), *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] newTask ) @@ -888,6 +935,24 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Removes wallpaper activity when leaving desktop + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() { val task = setUpFreeformTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -899,6 +964,44 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) + // Removes wallpaper activity when leaving desktop + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() { + val task1 = setUpFreeformTask() + // Setup task2 + setUpFreeformTask() + val wallpaperToken = MockToken().token() + + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val task1Change = assertNotNull(wct.changes[task1.token.asBinder()]) + assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Does not remove wallpaper activity, as desktop still has a visible desktop task + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test fun moveToFullscreen_nonExistentTask_doesNothing() { controller.moveToFullscreen(999, transitionSource = UNKNOWN) verifyExitDesktopWCTNotExecuted() @@ -1722,6 +1825,49 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) + desktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task3.taskId, + visible = false) + + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Does not remove wallpaper activity, as desktop still has visible desktop tasks + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { val spyController = spy(controller) @@ -1930,6 +2076,7 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(null)) } + @Test fun enterSplit_freeformTaskIsMovedToSplit() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -1939,14 +2086,67 @@ class DesktopTasksControllerTest : ShellTestCase() { task2.isFocused = true task3.isFocused = false - controller.enterSplit(DEFAULT_DISPLAY, false) + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) verify(splitScreenController) .requestEnterSplitSelect( - task2, + eq(task2), any(), - SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT, - task2.configuration.windowConfiguration.bounds) + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds)) + } + + @Test + fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) + desktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task3.taskId, + visible = false) + + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) + + val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(splitScreenController) + .requestEnterSplitSelect( + eq(task2), + wctArgument.capture(), + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds)) + // Removes wallpaper activity when leaving desktop + wctArgument.value.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) + + val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(splitScreenController) + .requestEnterSplitSelect( + eq(task2), + wctArgument.capture(), + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds)) + // Does not remove wallpaper activity, as desktop still has visible desktop tasks + assertThat(wctArgument.value.hierarchyOps).isEmpty() } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java index 754a173ff069..6bc7e499159c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java @@ -17,6 +17,7 @@ package com.android.wm.shell.transition; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_SLEEP; @@ -185,6 +186,73 @@ public class DefaultTransitionHandlerTest extends ShellTestCase { verify(startT, never()).setColor(any(), any()); } + @Test + public void startAnimation_freeformOpenChange_doesntReparentTask() { + final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN) + .setTask(createTaskInfo( + /* taskId= */ 1, /* windowingMode= */ WINDOWING_MODE_FULLSCREEN)) + .build(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openChange) + .build(); + final IBinder token = new Binder(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + verify(startT, never()).reparent(any(), any()); + } + + @Test + public void startAnimation_freeformMinimizeChange_underFullscreenChange_doesntReparentTask() { + final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN) + .setTask(createTaskInfo( + /* taskId= */ 1, /* windowingMode= */ WINDOWING_MODE_FULLSCREEN)) + .build(); + final TransitionInfo.Change toBackChange = new ChangeBuilder(TRANSIT_TO_BACK) + .setTask(createTaskInfo( + /* taskId= */ 2, /* windowingMode= */ WINDOWING_MODE_FREEFORM)) + .build(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openChange) + .addChange(toBackChange) + .build(); + final IBinder token = new Binder(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + verify(startT, never()).reparent(any(), any()); + } + + @Test + public void startAnimation_freeform_minimizeAnimation_reparentsTask() { + final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN) + .setTask(createTaskInfo( + /* taskId= */ 1, /* windowingMode= */ WINDOWING_MODE_FREEFORM)) + .build(); + final TransitionInfo.Change toBackChange = new ChangeBuilder(TRANSIT_TO_BACK) + .setTask(createTaskInfo( + /* taskId= */ 2, /* windowingMode= */ WINDOWING_MODE_FREEFORM)) + .build(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openChange) + .addChange(toBackChange) + .build(); + final IBinder token = new Binder(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + verify(startT).reparent(any(), any()); + } + private static void mergeSync(Transitions.TransitionHandler handler, IBinder token) { handler.mergeAnimation( new Binder(), @@ -195,10 +263,14 @@ public class DefaultTransitionHandlerTest extends ShellTestCase { } private static RunningTaskInfo createTaskInfo(int taskId) { + return createTaskInfo(taskId, WINDOWING_MODE_FULLSCREEN); + } + + private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.topActivityType = ACTIVITY_TYPE_STANDARD; - taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); taskInfo.configuration.windowConfiguration.setActivityType(taskInfo.topActivityType); taskInfo.token = mock(WindowContainerToken.class); return taskInfo; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionAnimationHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionAnimationHelperTest.kt new file mode 100644 index 000000000000..bad14bbdb141 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionAnimationHelperTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration +import android.view.WindowManager +import android.window.TransitionInfo +import android.window.TransitionInfo.FLAG_TRANSLUCENT +import com.android.internal.R +import com.android.internal.policy.TransitionAnimation +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import org.junit.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.verify + +class TransitionAnimationHelperTest : ShellTestCase() { + + @Mock + lateinit var transitionAnimation: TransitionAnimation + + @Test + fun loadAttributeAnimation_freeform_taskOpen_taskToBackChange_returnsMinimizeAnim() { + val openChange = ChangeBuilder(WindowManager.TRANSIT_OPEN) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM)) + .build() + val toBackChange = ChangeBuilder(WindowManager.TRANSIT_TO_BACK) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM)) + .build() + val info = TransitionInfoBuilder(WindowManager.TRANSIT_OPEN) + .addChange(openChange) + .addChange(toBackChange) + .build() + + loadAttributeAnimation(WindowManager.TRANSIT_OPEN, info, toBackChange) + + verify(transitionAnimation).loadDefaultAnimationAttr( + eq(R.styleable.WindowAnimation_activityCloseExitAnimation), anyBoolean()) + } + + @Test + fun loadAttributeAnimation_freeform_taskToFront_taskToFrontChange_returnsUnminimizeAnim() { + val toFrontChange = ChangeBuilder(WindowManager.TRANSIT_TO_FRONT) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM)) + .build() + val info = TransitionInfoBuilder(WindowManager.TRANSIT_TO_FRONT) + .addChange(toFrontChange) + .build() + + loadAttributeAnimation(WindowManager.TRANSIT_TO_FRONT, info, toFrontChange) + + verify(transitionAnimation).loadDefaultAnimationAttr( + eq(R.styleable.WindowAnimation_activityOpenEnterAnimation), + /* translucent= */ anyBoolean()) + } + + @Test + fun loadAttributeAnimation_fullscreen_taskOpen_returnsTaskOpenEnterAnim() { + val openChange = ChangeBuilder(WindowManager.TRANSIT_OPEN) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FULLSCREEN)) + .build() + val info = TransitionInfoBuilder(WindowManager.TRANSIT_OPEN).addChange(openChange).build() + + loadAttributeAnimation(WindowManager.TRANSIT_OPEN, info, openChange) + + verify(transitionAnimation).loadDefaultAnimationAttr( + eq(R.styleable.WindowAnimation_taskOpenEnterAnimation), + /* translucent= */ anyBoolean()) + } + + @Test + fun loadAttributeAnimation_freeform_taskOpen_taskToBackChange_passesTranslucent() { + val openChange = ChangeBuilder(WindowManager.TRANSIT_OPEN) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM)) + .build() + val toBackChange = ChangeBuilder(WindowManager.TRANSIT_TO_BACK) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM)) + .setFlags(FLAG_TRANSLUCENT) + .build() + val info = TransitionInfoBuilder(WindowManager.TRANSIT_OPEN) + .addChange(openChange) + .addChange(toBackChange) + .build() + + loadAttributeAnimation(WindowManager.TRANSIT_OPEN, info, toBackChange) + + verify(transitionAnimation).loadDefaultAnimationAttr( + eq(R.styleable.WindowAnimation_activityCloseExitAnimation), + /* translucent= */ eq(true)) + } + + private fun loadAttributeAnimation( + @WindowManager.TransitionType type: Int, + info: TransitionInfo, + change: TransitionInfo.Change, + wallpaperTransit: Int = TransitionAnimation.WALLPAPER_TRANSITION_NONE, + isDreamTransition: Boolean = false, + ) { + TransitionAnimationHelper.loadAttributeAnimation( + type, info, change, wallpaperTransit, transitionAnimation, isDreamTransition) + } + + private fun createTaskInfo(windowingMode: Int): RunningTaskInfo { + val taskInfo = TestRunningTaskInfoBuilder() + .setWindowingMode(windowingMode) + .build() + return taskInfo + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt new file mode 100644 index 000000000000..261d4b5e7d1a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor + +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.WindowInsetsController +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.google.common.truth.Truth +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class CaptionWindowDecorationTests : ShellTestCase() { + @Test + fun updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { + val taskInfo = createTaskInfo() + taskInfo.configuration.windowConfiguration.windowingMode = + WindowConfiguration.WINDOWING_MODE_FREEFORM + taskInfo.taskDescription!!.topOpaqueSystemBarsAppearance = + WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND + val relayoutParams = WindowDecoration.RelayoutParams() + + CaptionWindowDecoration.updateRelayoutParams( + relayoutParams, + taskInfo, + true, + false + ) + + Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isTrue() + } + + @Test + fun updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() { + val taskInfo = createTaskInfo() + taskInfo.configuration.windowConfiguration.windowingMode = + WindowConfiguration.WINDOWING_MODE_FREEFORM + taskInfo.taskDescription!!.topOpaqueSystemBarsAppearance = 0 + val relayoutParams = WindowDecoration.RelayoutParams() + + CaptionWindowDecoration.updateRelayoutParams( + relayoutParams, + taskInfo, + true, + false + ) + + Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isFalse() + } + + @Test + fun updateRelayoutParams_addOccludingCaptionElementCorrectly() { + val taskInfo = createTaskInfo() + val relayoutParams = WindowDecoration.RelayoutParams() + CaptionWindowDecoration.updateRelayoutParams( + relayoutParams, + taskInfo, + true, + false + ) + Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2) + Truth.assertThat(relayoutParams.mOccludingCaptionElements[0].mAlignment).isEqualTo( + WindowDecoration.RelayoutParams.OccludingCaptionElement.Alignment.START) + Truth.assertThat(relayoutParams.mOccludingCaptionElements[1].mAlignment).isEqualTo( + WindowDecoration.RelayoutParams.OccludingCaptionElement.Alignment.END) + } + + private fun createTaskInfo(): ActivityManager.RunningTaskInfo { + val taskDescriptionBuilder = + ActivityManager.TaskDescription.Builder() + val taskInfo = TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setVisible(true) + .build() + taskInfo.realActivity = ComponentName( + "com.android.wm.shell.windowdecor", + "CaptionWindowDecorationTests" + ) + taskInfo.baseActivity = ComponentName( + "com.android.wm.shell.windowdecor", + "CaptionWindowDecorationTests" + ) + return taskInfo + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index 0c50ab6b5008..adda9a688172 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -22,10 +22,10 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.graphics.Bitmap import android.graphics.Color +import android.graphics.Point import android.graphics.Rect -import android.platform.test.annotations.RequiresFlagsEnabled -import android.platform.test.flag.junit.CheckFlagsRule -import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display @@ -33,6 +33,7 @@ import android.view.LayoutInflater import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.View +import androidx.core.graphics.toPointF import androidx.test.filters.SmallTest import com.android.window.flags.Flags import com.android.wm.shell.R @@ -46,6 +47,7 @@ import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UND import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -69,7 +71,7 @@ import org.mockito.kotlin.whenever class HandleMenuTest : ShellTestCase() { @JvmField @Rule - val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + val setFlagsRule: SetFlagsRule = SetFlagsRule() @Mock private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration @@ -92,6 +94,8 @@ class HandleMenuTest : ShellTestCase() { private lateinit var handleMenu: HandleMenu + private val menuWidthWithElevation = MENU_WIDTH + MENU_PILL_ELEVATION + @Before fun setUp() { val mockAdditionalViewHostViewContainer = AdditionalViewHostViewContainer( @@ -100,7 +104,7 @@ class HandleMenuTest : ShellTestCase() { ) { SurfaceControl.Transaction() } - val menuView = LayoutInflater.from(context).inflate( + val menuView = LayoutInflater.from(mContext).inflate( R.layout.desktop_mode_window_decor_handle_menu, null) whenever(mockDesktopWindowDecoration.addWindow( anyInt(), any(), any(), any(), anyInt(), anyInt(), anyInt(), anyInt()) @@ -110,50 +114,69 @@ class HandleMenuTest : ShellTestCase() { whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) whenever(displayLayout.isLandscape).thenReturn(true) - mockDesktopWindowDecoration.mDecorWindowContext = context + mContext.orCreateTestableResources.apply { + addOverride(R.dimen.desktop_mode_handle_menu_width, MENU_WIDTH) + addOverride(R.dimen.desktop_mode_handle_menu_height, MENU_HEIGHT) + addOverride(R.dimen.desktop_mode_handle_menu_margin_top, MENU_TOP_MARGIN) + addOverride(R.dimen.desktop_mode_handle_menu_margin_start, MENU_START_MARGIN) + addOverride(R.dimen.desktop_mode_handle_menu_pill_elevation, MENU_PILL_ELEVATION) + addOverride( + R.dimen.desktop_mode_handle_menu_pill_spacing_margin, MENU_PILL_SPACING_MARGIN) + } + mockDesktopWindowDecoration.mDecorWindowContext = mContext } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testFullscreenMenuUsesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED) val handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of display. - assertTrue(handleMenu.mHandleMenuPosition.equals(16f, -512f)) + val expected = Point(DISPLAY_BOUNDS.centerX() - menuWidthWithElevation / 2, MENU_TOP_MARGIN) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testFreeformMenu_usesViewHostViewContainer() { createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED) handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalViewHostViewContainer) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalViewHostViewContainer) // Verify menu is created near top-left of task. - assertTrue(handleMenu.mHandleMenuPosition.equals(12f, 8f)) + val expected = Point(MENU_START_MARGIN, MENU_TOP_MARGIN) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testSplitLeftMenu_usesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT) handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, - // show at the top of split left task. - assertTrue(handleMenu.mHandleMenuPosition.equals(-624f, -512f)) + // show at the top-center of split left task. + val expected = Point( + SPLIT_LEFT_BOUNDS.centerX() - menuWidthWithElevation / 2, + MENU_TOP_MARGIN + ) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testSplitRightMenu_usesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT) handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, - // show at the top of split right task. - assertTrue(handleMenu.mHandleMenuPosition.equals(656f, -512f)) + // show at the top-center of split right task. + val expected = Point( + SPLIT_RIGHT_BOUNDS.centerX() - menuWidthWithElevation / 2, + MENU_TOP_MARGIN + ) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } private fun createTaskInfo(windowingMode: Int, splitPosition: Int) { @@ -178,14 +201,10 @@ class HandleMenuTest : ShellTestCase() { .setBounds(bounds) .setVisible(true) .build() - // Calculate captionX similar to how WindowDecoration calculates it. - whenever(mockDesktopWindowDecoration.captionX).thenReturn( - (mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration - .bounds.width() - context.resources.getDimensionPixelSize( - R.dimen.desktop_mode_fullscreen_decor_caption_width)) / 2) whenever(splitScreenController.getSplitPosition(any())).thenReturn(splitPosition) whenever(splitScreenController.getStageBounds(any(), any())).thenAnswer { (it.arguments.first() as Rect).set(SPLIT_LEFT_BOUNDS) + (it.arguments[1] as Rect).set(SPLIT_RIGHT_BOUNDS) } } @@ -193,7 +212,7 @@ class HandleMenuTest : ShellTestCase() { val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) { R.layout.desktop_mode_app_header } else { - R.layout.desktop_mode_app_header + R.layout.desktop_mode_app_handle } val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId, onClickListener, onTouchListener, appIcon, appName, displayController, @@ -208,5 +227,11 @@ class HandleMenuTest : ShellTestCase() { private val FREEFORM_BOUNDS = Rect(500, 500, 2000, 1200) private val SPLIT_LEFT_BOUNDS = Rect(0, 0, 1280, 1600) private val SPLIT_RIGHT_BOUNDS = Rect(1280, 0, 2560, 1600) + private const val MENU_WIDTH = 200 + private const val MENU_HEIGHT = 400 + private const val MENU_TOP_MARGIN = 10 + private const val MENU_START_MARGIN = 20 + private const val MENU_PILL_ELEVATION = 2 + private const val MENU_PILL_SPACING_MARGIN = 4 } } 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 31c6479195c3..2d1bf14ffbb3 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 @@ -18,6 +18,8 @@ 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.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; +import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.mandatorySystemGestures; import static android.view.WindowInsets.Type.statusBars; @@ -54,6 +56,8 @@ import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; import android.view.AttachedSurfaceControl; @@ -71,6 +75,7 @@ import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; @@ -80,6 +85,7 @@ import com.android.wm.shell.tests.R; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -105,6 +111,9 @@ public class WindowDecorationTests extends ShellTestCase { private static final int CORNER_RADIUS = 20; private static final int STATUS_BAR_INSET_SOURCE_ID = 0; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult = new WindowDecoration.RelayoutResult<>(); @@ -260,7 +269,8 @@ public class WindowDecorationTests extends ShellTestCase { eq(0 /* index */), eq(WindowInsets.Type.captionBar()), eq(new Rect(100, 300, 400, 364)), - any()); + any(), + anyInt()); verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); verify(mMockSurfaceControlFinishT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); @@ -565,9 +575,9 @@ public class WindowDecorationTests extends ShellTestCase { windowDecor.relayout(taskInfo); verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(captionBar()), any(), any()); + eq(0) /* index */, eq(captionBar()), any(), any(), anyInt()); verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any(), anyInt()); } @Test @@ -654,9 +664,9 @@ public class WindowDecorationTests extends ShellTestCase { // Never added. verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(captionBar()), any(), any()); + eq(0) /* index */, eq(captionBar()), any(), any(), anyInt()); verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any(), anyInt()); // No need to remove them if they were never added. verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token), any(), eq(0) /* index */, eq(captionBar())); @@ -681,9 +691,9 @@ public class WindowDecorationTests extends ShellTestCase { mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true); windowDecor.relayout(taskInfo); verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(captionBar()), any(), any()); + eq(0) /* index */, eq(captionBar()), any(), any(), anyInt()); verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any(), anyInt()); windowDecor.close(); @@ -738,9 +748,9 @@ public class WindowDecorationTests extends ShellTestCase { // Insets should be applied twice. verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(), - eq(0) /* index */, eq(captionBar()), any(), any()); + eq(0) /* index */, eq(captionBar()), any(), any(), anyInt()); verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any(), anyInt()); } @Test @@ -765,9 +775,30 @@ public class WindowDecorationTests extends ShellTestCase { // Insets should only need to be applied once. verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(), - eq(0) /* index */, eq(captionBar()), any(), any()); + eq(0) /* index */, eq(captionBar()), any(), any(), anyInt()); verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION) + public void testRelayout_captionInsetForceConsume() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + final WindowContainerToken token = TestRunningTaskInfoBuilder.createMockWCToken(); + final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setVisible(true); + + final ActivityManager.RunningTaskInfo taskInfo = + builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build(); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); + windowDecor.relayout(taskInfo); + + // Caption inset source should be force-consuming. + verify(mMockWindowContainerTransaction).addInsetsSource(eq(token), any(), + eq(0) /* index */, eq(captionBar()), any(), any(), eq(FLAG_FORCE_CONSUMING)); } @Test diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 341599e79662..e302fa8b1fc3 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -114,16 +114,12 @@ cc_defaults { "libbase", "libharfbuzz_ng", "libminikin", - "server_configurable_flags", - "libaconfig_storage_read_api_cc" ], static_libs: [ "libui-types", ], - whole_static_libs: ["hwui_flags_cc_lib"], - target: { android: { shared_libs: [ @@ -145,6 +141,8 @@ cc_defaults { "libsync", "libui", "aconfig_text_flags_c_lib", + "server_configurable_flags", + "libaconfig_storage_read_api_cc", ], static_libs: [ "libEGL_blobCache", @@ -155,6 +153,7 @@ cc_defaults { "libstatssocket_lazy", "libtonemap", ], + whole_static_libs: ["hwui_flags_cc_lib"], }, host: { static_libs: [ @@ -419,7 +418,6 @@ cc_defaults { ], static_libs: [ - "libnativehelper_lazy", "libziparchive_for_incfs", ], @@ -446,6 +444,7 @@ cc_defaults { ], static_libs: [ "libgif", + "libnativehelper_lazy", "libstatslog_hwui", "libstatspull_lazy", "libstatssocket_lazy", @@ -464,6 +463,7 @@ cc_defaults { ], static_libs: [ "libandroidfw", + "libnativehelper_jvm", ], }, }, diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp index 70a9ef04d6f3..b4e6b7243ddc 100644 --- a/libs/hwui/apex/LayoutlibLoader.cpp +++ b/libs/hwui/apex/LayoutlibLoader.cpp @@ -28,6 +28,7 @@ using namespace std; extern int register_android_graphics_Bitmap(JNIEnv*); extern int register_android_graphics_BitmapFactory(JNIEnv*); +extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*); extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env); extern int register_android_graphics_Camera(JNIEnv* env); extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env); @@ -41,6 +42,7 @@ extern int register_android_graphics_Shader(JNIEnv* env); extern int register_android_graphics_RenderEffect(JNIEnv* env); extern int register_android_graphics_Typeface(JNIEnv* env); extern int register_android_graphics_YuvImage(JNIEnv* env); +extern int register_android_graphics_drawable_AnimatedImageDrawable(JNIEnv* env); namespace android { @@ -51,7 +53,12 @@ extern int register_android_graphics_ColorFilter(JNIEnv* env); extern int register_android_graphics_ColorSpace(JNIEnv* env); extern int register_android_graphics_DrawFilter(JNIEnv* env); extern int register_android_graphics_FontFamily(JNIEnv* env); +extern int register_android_graphics_Gainmap(JNIEnv* env); +extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env); +extern int register_android_graphics_HardwareRendererObserver(JNIEnv* env); extern int register_android_graphics_Matrix(JNIEnv* env); +extern int register_android_graphics_Mesh(JNIEnv* env); +extern int register_android_graphics_MeshSpecification(JNIEnv* env); extern int register_android_graphics_Paint(JNIEnv* env); extern int register_android_graphics_Path(JNIEnv* env); extern int register_android_graphics_PathIterator(JNIEnv* env); @@ -72,6 +79,7 @@ extern int register_android_graphics_text_GraphemeBreak(JNIEnv* env); extern int register_android_util_PathParser(JNIEnv* env); extern int register_android_view_DisplayListCanvas(JNIEnv* env); extern int register_android_view_RenderNode(JNIEnv* env); +extern int register_android_view_ThreadedRenderer(JNIEnv* env); #define REG_JNI(name) { name } struct RegJNIRec { @@ -83,6 +91,8 @@ struct RegJNIRec { static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.graphics.Bitmap", REG_JNI(register_android_graphics_Bitmap)}, {"android.graphics.BitmapFactory", REG_JNI(register_android_graphics_BitmapFactory)}, + {"android.graphics.BitmapRegionDecoder", + REG_JNI(register_android_graphics_BitmapRegionDecoder)}, {"android.graphics.ByteBufferStreamAdaptor", REG_JNI(register_android_graphics_ByteBufferStreamAdaptor)}, {"android.graphics.Camera", REG_JNI(register_android_graphics_Camera)}, @@ -95,11 +105,20 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor)}, {"android.graphics.DrawFilter", REG_JNI(register_android_graphics_DrawFilter)}, {"android.graphics.FontFamily", REG_JNI(register_android_graphics_FontFamily)}, + {"android.graphics.Gainmap", REG_JNI(register_android_graphics_Gainmap)}, {"android.graphics.Graphics", REG_JNI(register_android_graphics_Graphics)}, + {"android.graphics.HardwareBufferRenderer", + REG_JNI(register_android_graphics_HardwareBufferRenderer)}, + {"android.graphics.HardwareRenderer", REG_JNI(register_android_view_ThreadedRenderer)}, + {"android.graphics.HardwareRendererObserver", + REG_JNI(register_android_graphics_HardwareRendererObserver)}, {"android.graphics.ImageDecoder", REG_JNI(register_android_graphics_ImageDecoder)}, {"android.graphics.Interpolator", REG_JNI(register_android_graphics_Interpolator)}, {"android.graphics.MaskFilter", REG_JNI(register_android_graphics_MaskFilter)}, {"android.graphics.Matrix", REG_JNI(register_android_graphics_Matrix)}, + {"android.graphics.Mesh", REG_JNI(register_android_graphics_Mesh)}, + {"android.graphics.MeshSpecification", + REG_JNI(register_android_graphics_MeshSpecification)}, {"android.graphics.NinePatch", REG_JNI(register_android_graphics_NinePatch)}, {"android.graphics.Paint", REG_JNI(register_android_graphics_Paint)}, {"android.graphics.Path", REG_JNI(register_android_graphics_Path)}, @@ -118,6 +137,8 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory)}, {"android.graphics.animation.RenderNodeAnimator", REG_JNI(register_android_graphics_animation_RenderNodeAnimator)}, + {"android.graphics.drawable.AnimatedImageDrawable", + REG_JNI(register_android_graphics_drawable_AnimatedImageDrawable)}, {"android.graphics.drawable.AnimatedVectorDrawable", REG_JNI(register_android_graphics_drawable_AnimatedVectorDrawable)}, {"android.graphics.drawable.VectorDrawable", diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index cfca48084d97..0efb2c81af01 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -17,7 +17,6 @@ #include <SkFontMetrics.h> #include <SkRRect.h> #include <SkTextBlob.h> -#include <com_android_graphics_hwui_flags.h> #include "../utils/Color.h" #include "Canvas.h" @@ -30,7 +29,19 @@ #include "hwui/PaintFilter.h" #include "pipeline/skia/SkiaRecordingCanvas.h" +#ifdef __ANDROID__ +#include <com_android_graphics_hwui_flags.h> namespace flags = com::android::graphics::hwui::flags; +#else +namespace flags { +constexpr bool high_contrast_text_luminance() { + return false; +} +constexpr bool high_contrast_text_small_text_rect() { + return false; +} +} // namespace flags +#endif namespace android { diff --git a/libs/hwui/jni/MeshSpecification.cpp b/libs/hwui/jni/MeshSpecification.cpp index ae9792df3d82..b943496ae9f7 100644 --- a/libs/hwui/jni/MeshSpecification.cpp +++ b/libs/hwui/jni/MeshSpecification.cpp @@ -126,7 +126,7 @@ static void MeshSpecification_safeUnref(SkMeshSpecification* meshSpec) { SkSafeUnref(meshSpec); } -static jlong getMeshSpecificationFinalizer() { +static jlong getMeshSpecificationFinalizer(CRITICAL_JNI_PARAMS) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(&MeshSpecification_safeUnref)); } diff --git a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp index e3cdee6e7034..3b1b86160f3a 100644 --- a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp @@ -135,7 +135,7 @@ static void android_graphics_HardwareBufferRenderer_setLightAlpha(JNIEnv* env, j proxy->setLightAlpha((uint8_t)(255 * ambientShadowAlpha), (uint8_t)(255 * spotShadowAlpha)); } -static jlong android_graphics_HardwareBufferRenderer_getFinalizer() { +static jlong android_graphics_HardwareBufferRenderer_getFinalizer(CRITICAL_JNI_PARAMS) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(&HardwareBufferRenderer_destroy)); } diff --git a/location/Android.bp b/location/Android.bp index c0e102acad4d..bc02d1f852de 100644 --- a/location/Android.bp +++ b/location/Android.bp @@ -30,9 +30,6 @@ java_sdk_library { "app-compat-annotations", "unsupportedappusage", // for android.compat.annotation.UnsupportedAppUsage ], - hidden_api_packages: [ - "com.android.internal.location", - ], aidl: { include_dirs: [ "frameworks/base/location/java", diff --git a/location/java/com/android/internal/location/package-info.java b/location/java/com/android/internal/location/package-info.java new file mode 100644 index 000000000000..25573c15ce59 --- /dev/null +++ b/location/java/com/android/internal/location/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Exclude from API surfaces + * + * @hide + */ +package com.android.internal.location; diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 73deb17d0055..03cd53580b1b 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -1797,6 +1797,7 @@ public class AudioTrack extends PlayerBase (flags != 0 // cannot have any special flags || attributes.getUsage() != AudioAttributes.USAGE_MEDIA || (attributes.getContentType() != AudioAttributes.CONTENT_TYPE_UNKNOWN + && attributes.getContentType() != AudioAttributes.CONTENT_TYPE_SPEECH && attributes.getContentType() != AudioAttributes.CONTENT_TYPE_MUSIC && attributes.getContentType() != AudioAttributes.CONTENT_TYPE_MOVIE))) { return false; diff --git a/nfc/Android.bp b/nfc/Android.bp index 13ac2311bde3..0282e6f5c246 100644 --- a/nfc/Android.bp +++ b/nfc/Android.bp @@ -61,9 +61,6 @@ java_sdk_library { "android.nfc", "com.android.nfc", ], - hidden_api_packages: [ - "com.android.nfc", - ], impl_library_visibility: [ "//frameworks/base:__subpackages__", "//cts/hostsidetests/multidevices/nfc:__subpackages__", diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 9ce1c8257c17..395f81d73351 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -963,22 +963,9 @@ public final class NfcAdapter { throw new UnsupportedOperationException("You need a context on NfcAdapter to use the " + " NFC extras APIs"); } - try { - return sService.getNfcDtaInterface(mContext.getPackageName()); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return null; - } - try { - return sService.getNfcDtaInterface(mContext.getPackageName()); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return null; - } + return callServiceReturn(() -> sService.getNfcDtaInterface(mContext.getPackageName()), + null); + } /** @@ -1095,22 +1082,8 @@ public final class NfcAdapter { @SystemApi @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public @AdapterState int getAdapterState() { - try { - return sService.getState(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return NfcAdapter.STATE_OFF; - } - try { - return sService.getState(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return NfcAdapter.STATE_OFF; - } + return callServiceReturn(() -> sService.getState(), NfcAdapter.STATE_OFF); + } /** @@ -1134,22 +1107,8 @@ public final class NfcAdapter { @FlaggedApi(Flags.FLAG_NFC_STATE_CHANGE) @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable() { - try { - return sService.enable(mContext.getPackageName()); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.enable(mContext.getPackageName()); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.enable(mContext.getPackageName()), false); + } /** @@ -1175,22 +1134,9 @@ public final class NfcAdapter { @FlaggedApi(Flags.FLAG_NFC_STATE_CHANGE) @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable() { - try { - return sService.disable(true, mContext.getPackageName()); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.disable(true, mContext.getPackageName()); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.disable(true, mContext.getPackageName()), + false); + } /** @@ -1200,22 +1146,9 @@ public final class NfcAdapter { @SystemApi @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable(boolean persist) { - try { - return sService.disable(persist, mContext.getPackageName()); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.disable(persist, mContext.getPackageName()); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.disable(persist, mContext.getPackageName()), + false); + } /** @@ -1241,12 +1174,7 @@ public final class NfcAdapter { */ @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) public boolean isObserveModeSupported() { - try { - return sService.isObserveModeSupported(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - return false; - } + return callServiceReturn(() -> sService.isObserveModeSupported(), false); } /** @@ -1257,12 +1185,7 @@ public final class NfcAdapter { @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) public boolean isObserveModeEnabled() { - try { - return sService.isObserveModeEnabled(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - return false; - } + return callServiceReturn(() -> sService.isObserveModeEnabled(), false); } /** @@ -1286,12 +1209,8 @@ public final class NfcAdapter { throw new UnsupportedOperationException("You need a context on NfcAdapter to use the " + " observe mode APIs"); } - try { - return sService.setObserveMode(enabled, mContext.getPackageName()); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - return false; - } + return callServiceReturn(() -> sService.setObserveMode(enabled, mContext.getPackageName()), + false); } /** @@ -2057,22 +1976,8 @@ public final class NfcAdapter { if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } - try { - return sService.setNfcSecure(enable); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.setNfcSecure(enable); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.setNfcSecure(enable), false); + } /** @@ -2088,22 +1993,8 @@ public final class NfcAdapter { if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } - try { - return sService.deviceSupportsNfcSecure(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.deviceSupportsNfcSecure(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.deviceSupportsNfcSecure(), false); + } /** @@ -2121,22 +2012,8 @@ public final class NfcAdapter { if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } - try { - return sService.getNfcAntennaInfo(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return null; - } - try { - return sService.getNfcAntennaInfo(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return null; - } + return callServiceReturn(() -> sService.getNfcAntennaInfo(), null); + } /** @@ -2154,22 +2031,8 @@ public final class NfcAdapter { if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } - try { - return sService.isNfcSecureEnabled(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.isNfcSecureEnabled(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.isNfcSecureEnabled(), false); + } /** @@ -2185,22 +2048,8 @@ public final class NfcAdapter { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - try { - return sService.enableReaderOption(enable); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.enableReaderOption(enable); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.enableReaderOption(enable), false); + } /** @@ -2214,22 +2063,8 @@ public final class NfcAdapter { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - try { - return sService.isReaderOptionSupported(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.isReaderOptionSupported(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.isReaderOptionSupported(), false); + } /** @@ -2245,22 +2080,8 @@ public final class NfcAdapter { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - try { - return sService.isReaderOptionEnabled(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.isReaderOptionEnabled(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.isReaderOptionEnabled(), false); + } /** @@ -2388,11 +2209,9 @@ public final class NfcAdapter { synchronized (mLock) { mTagRemovedListener = iListener; } - try { - return sService.ignore(tag.getServiceHandle(), debounceMs, iListener); - } catch (RemoteException e) { - return false; - } + final ITagRemovedCallback.Stub passedListener = iListener; + return callServiceReturn(() -> + sService.ignore(tag.getServiceHandle(), debounceMs, passedListener), false); } /** @@ -2509,22 +2328,9 @@ public final class NfcAdapter { throw new UnsupportedOperationException("You need a context on NfcAdapter to use the " + " NFC extras APIs"); } - try { - return sService.getNfcAdapterExtrasInterface(mContext.getPackageName()); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return null; - } - try { - return sService.getNfcAdapterExtrasInterface(mContext.getPackageName()); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return null; - } + return callServiceReturn(() -> + sService.getNfcAdapterExtrasInterface(mContext.getPackageName()), null); + } void enforceResumed(Activity activity) { @@ -2569,22 +2375,8 @@ public final class NfcAdapter { if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } - try { - return sService.setControllerAlwaysOn(value); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.setControllerAlwaysOn(value); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.setControllerAlwaysOn(value), false); + } /** @@ -2600,22 +2392,8 @@ public final class NfcAdapter { @SystemApi @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOn() { - try { - return sService.isControllerAlwaysOn(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.isControllerAlwaysOn(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.isControllerAlwaysOn(), false); + } /** @@ -2634,22 +2412,8 @@ public final class NfcAdapter { if (!sHasNfcFeature && !sHasCeFeature) { throw new UnsupportedOperationException(); } - try { - return sService.isControllerAlwaysOnSupported(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.isControllerAlwaysOnSupported(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.isControllerAlwaysOnSupported(), false); + } /** @@ -2719,21 +2483,9 @@ public final class NfcAdapter { Log.e(TAG, "TagIntentAppPreference is not supported"); throw new UnsupportedOperationException(); } - try { - return sService.setTagIntentAppPreferenceForUser(userId, pkg, allow); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - } - try { - return sService.setTagIntentAppPreferenceForUser(userId, pkg, allow); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return TAG_INTENT_APP_PREF_RESULT_UNAVAILABLE; - } + return callServiceReturn(() -> + sService.setTagIntentAppPreferenceForUser(userId, pkg, allow), + TAG_INTENT_APP_PREF_RESULT_UNAVAILABLE); } @@ -2808,22 +2560,8 @@ public final class NfcAdapter { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - try { - return sService.isTagIntentAppPreferenceSupported(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.isTagIntentAppPreferenceSupported(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.isTagIntentAppPreferenceSupported(), false); + } /** @@ -2836,11 +2574,30 @@ public final class NfcAdapter { @TestApi @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void notifyPollingLoop(@NonNull PollingFrame pollingFrame) { + callService(() -> sService.notifyPollingLoop(pollingFrame)); + } + + + /** + * Notifies the system of new HCE data for tests. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public void notifyTestHceData(int technology, byte[] data) { + callService(() -> sService.notifyTestHceData(technology, data)); + } + + interface ServiceCall { + void call() throws RemoteException; + } + + void callService(ServiceCall call) { try { if (sService == null) { attemptDeadServiceRecovery(null); } - sService.notifyPollingLoop(pollingFrame); + call.call(); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time @@ -2849,38 +2606,35 @@ public final class NfcAdapter { return; } try { - sService.notifyPollingLoop(pollingFrame); + call.call(); } catch (RemoteException ee) { Log.e(TAG, "Failed to recover NFC Service."); } } } - - /** - * Notifies the system of new HCE data for tests. - * - * @hide - */ - @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) - public void notifyTestHceData(int technology, byte[] data) { + interface ServiceCallReturn<T> { + T call() throws RemoteException; + } + <T> T callServiceReturn(ServiceCallReturn<T> call, T defaultReturn) { try { if (sService == null) { attemptDeadServiceRecovery(null); } - sService.notifyTestHceData(technology, data); + return call.call(); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time if (sService == null) { Log.e(TAG, "Failed to recover NFC Service."); - return; + return defaultReturn; } try { - sService.notifyTestHceData(technology, data); - } catch (RemoteException e2) { + return call.call(); + } catch (RemoteException ee) { Log.e(TAG, "Failed to recover NFC Service."); } } + return defaultReturn; } /** @@ -2891,24 +2645,7 @@ public final class NfcAdapter { @TestApi @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void notifyHceDeactivated() { - try { - if (sService == null) { - attemptDeadServiceRecovery(null); - } - sService.notifyHceDeactivated(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return; - } - try { - sService.notifyHceDeactivated(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - } + callService(() -> sService.notifyHceDeactivated()); } /** @@ -2924,22 +2661,7 @@ public final class NfcAdapter { if (!sHasNfcWlcFeature) { throw new UnsupportedOperationException(); } - try { - return sService.setWlcEnabled(enable); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.setWlcEnabled(enable); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.setWlcEnabled(enable), false); } /** @@ -2954,22 +2676,8 @@ public final class NfcAdapter { if (!sHasNfcWlcFeature) { throw new UnsupportedOperationException(); } - try { - return sService.isWlcEnabled(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - return sService.isWlcEnabled(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return false; - } + return callServiceReturn(() -> sService.isWlcEnabled(), false); + } /** @@ -3048,22 +2756,8 @@ public final class NfcAdapter { if (!sHasNfcWlcFeature) { throw new UnsupportedOperationException(); } - try { - return sService.getWlcListenerDeviceInfo(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return null; - } - try { - return sService.getWlcListenerDeviceInfo(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return null; - } + return callServiceReturn(() -> sService.getWlcListenerDeviceInfo(), null); + } /** diff --git a/packages/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig index 225f8c685fe1..52e0cbbda03a 100644 --- a/packages/CrashRecovery/aconfig/flags.aconfig +++ b/packages/CrashRecovery/aconfig/flags.aconfig @@ -31,3 +31,11 @@ flag { description: "Deletes flag and settings resets" bug: "333847376" } + +flag { + name: "refactor_crashrecovery" + namespace: "modularization" + description: "Refactor required CrashRecovery code" + bug: "289203818" + is_fixed_read_only: true +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index c260426d47f5..88770d487a83 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -206,7 +206,7 @@ class InstallRepository(private val context: Context) { return InstallAborted(ABORT_REASON_INTERNAL_ERROR) } - val restriction = getDevicePolicyRestrictions() + val restriction = getDevicePolicyRestrictions(isTrustedSource) if (restriction != null) { val adminSupportDetailsIntent = devicePolicyManager!!.createAdminSupportIntent(restriction) @@ -237,18 +237,25 @@ class InstallRepository(private val context: Context) { intent: Intent, callingUid: Int, ): Boolean { - val isNotUnknownSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false) - return (sourceInfo != null && sourceInfo.isPrivilegedApp - && (isNotUnknownSource - || isPermissionGranted(context, Manifest.permission.INSTALL_PACKAGES, callingUid))) + val isPrivilegedAndKnown = sourceInfo != null && sourceInfo.isPrivilegedApp && + intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false) + val isInstallPkgPermissionGranted = + isPermissionGranted(context, Manifest.permission.INSTALL_PACKAGES, callingUid) + + return isPrivilegedAndKnown || isInstallPkgPermissionGranted } - private fun getDevicePolicyRestrictions(): String? { - val restrictions = arrayOf( - UserManager.DISALLOW_INSTALL_APPS, - UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, - UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY - ) + private fun getDevicePolicyRestrictions(isTrustedSource: Boolean): String? { + val restrictions: Array<String> = if (isTrustedSource) { + arrayOf(UserManager.DISALLOW_INSTALL_APPS) + } else { + arrayOf( + UserManager.DISALLOW_INSTALL_APPS, + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY + ) + } + for (restriction in restrictions) { if (!userManager!!.hasUserRestrictionForUser(restriction, Process.myUserHandle())) { continue diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt index 85728528a25c..828a95fcbb01 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt @@ -130,8 +130,8 @@ object PackageUtil { * @param context the [Context] object * @param callingUid the UID of the caller of Pia * @param isTrustedSource indicates whether install request is coming from a privileged app - * that has passed EXTRA_NOT_UNKNOWN_SOURCE as `true` in the installation intent, or that has - * the [INSTALL_PACKAGES][Manifest.permission.INSTALL_PACKAGES] permission granted. + * that has passed EXTRA_NOT_UNKNOWN_SOURCE as `true` in the installation intent, or an app that + * has the [INSTALL_PACKAGES][Manifest.permission.INSTALL_PACKAGES] permission granted. * * @return `true` if the package is either a system downloads provider, a document manager, * a trusted source, or has declared the diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt index 14af5084d625..296579333115 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt @@ -16,7 +16,6 @@ package com.android.settingslib.spa.debug -import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -41,8 +40,6 @@ import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.util.SESSION_BROWSE import com.android.settingslib.spa.framework.util.SESSION_SEARCH import com.android.settingslib.spa.framework.util.createIntent -import com.android.settingslib.spa.slice.fromEntry -import com.android.settingslib.spa.slice.presenter.SliceDemo import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.scaffold.HomeScaffold @@ -52,7 +49,6 @@ private const val TAG = "DebugActivity" private const val ROUTE_ROOT = "root" private const val ROUTE_All_PAGES = "pages" private const val ROUTE_All_ENTRIES = "entries" -private const val ROUTE_All_SLICES = "slices" private const val ROUTE_PAGE = "page" private const val ROUTE_ENTRY = "entry" private const val PARAM_NAME_PAGE_ID = "pid" @@ -87,7 +83,6 @@ class DebugActivity : ComponentActivity() { composable(route = ROUTE_ROOT) { RootPage() } composable(route = ROUTE_All_PAGES) { AllPages() } composable(route = ROUTE_All_ENTRIES) { AllEntries() } - composable(route = ROUTE_All_SLICES) { AllSlices() } composable( route = "$ROUTE_PAGE/{$PARAM_NAME_PAGE_ID}", arguments = listOf( @@ -109,8 +104,6 @@ class DebugActivity : ComponentActivity() { val entryRepository by spaEnvironment.entryRepository val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() } val allEntry = remember { entryRepository.getAllEntries() } - val allSliceEntry = - remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } } HomeScaffold(title = "Settings Debug") { Preference(object : PreferenceModel { override val title = "List All Pages (${allPageWithEntry.size})" @@ -120,10 +113,6 @@ class DebugActivity : ComponentActivity() { override val title = "List All Entries (${allEntry.size})" override val onClick = navigator(route = ROUTE_All_ENTRIES) }) - Preference(object : PreferenceModel { - override val title = "List All Slices (${allSliceEntry.size})" - override val onClick = navigator(route = ROUTE_All_SLICES) - }) } } @@ -152,18 +141,6 @@ class DebugActivity : ComponentActivity() { } } - @Composable - fun AllSlices() { - val entryRepository by spaEnvironment.entryRepository - val authority = spaEnvironment.sliceProviderAuthorities - val allSliceEntry = - remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } } - RegularScaffold(title = "All Slices (${allSliceEntry.size})") { - for (entry in allSliceEntry) { - SliceDemo(sliceUri = Uri.Builder().fromEntry(entry, authority).build()) - } - } - } @Composable fun OnePage(arguments: Bundle?) { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt index 444a3f0fd634..06d105ba61d8 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt @@ -70,7 +70,6 @@ fun SettingsEntry.debugContent(entryRepository: SettingsEntryRepository): String "allowSearch = $isAllowSearch", "isSearchDynamic = $isSearchDataDynamic", "isSearchMutable = $hasMutableStatus", - "hasSlice = $hasSliceSupport", "------ SEARCH ------", "search_path = $entryPathWithTitle", searchData?.debugContent() ?: "no search data", diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt index 780933d3c9e2..e5bbb8f027bf 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt @@ -50,7 +50,6 @@ enum class ColumnEnum(val id: String) { INTENT_TARGET_PACKAGE("intentTargetPackage"), INTENT_TARGET_CLASS("intentTargetClass"), INTENT_EXTRAS("intentExtras"), - SLICE_URI("sliceUri"), ENTRY_DISABLED("entryDisabled"), } @@ -71,7 +70,6 @@ enum class QueryEnum( ColumnEnum.INTENT_TARGET_PACKAGE, ColumnEnum.INTENT_TARGET_CLASS, ColumnEnum.INTENT_EXTRAS, - ColumnEnum.SLICE_URI, ) ), SEARCH_DYNAMIC_DATA_QUERY( @@ -85,7 +83,6 @@ enum class QueryEnum( ColumnEnum.INTENT_TARGET_PACKAGE, ColumnEnum.INTENT_TARGET_CLASS, ColumnEnum.INTENT_EXTRAS, - ColumnEnum.SLICE_URI, ) ), SEARCH_IMMUTABLE_STATUS_DATA_QUERY( @@ -115,7 +112,6 @@ enum class QueryEnum( ColumnEnum.INTENT_TARGET_PACKAGE, ColumnEnum.INTENT_TARGET_CLASS, ColumnEnum.INTENT_EXTRAS, - ColumnEnum.SLICE_URI, ColumnEnum.ENTRY_DISABLED, ) ), @@ -130,7 +126,6 @@ enum class QueryEnum( ColumnEnum.INTENT_TARGET_PACKAGE, ColumnEnum.INTENT_TARGET_CLASS, ColumnEnum.INTENT_EXTRAS, - ColumnEnum.SLICE_URI, ColumnEnum.ENTRY_DISABLED, ) ), diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt index eacb28c29bc3..65f700cb8b94 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt @@ -32,8 +32,6 @@ import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.util.SESSION_SEARCH import com.android.settingslib.spa.framework.util.createIntent -import com.android.settingslib.spa.slice.fromEntry - private const val TAG = "SpaSearchProvider" @@ -217,11 +215,6 @@ class SpaSearchProvider : ContentProvider() { .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name) .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras)) } - if (entry.hasSliceSupport) - row.add( - ColumnEnum.SLICE_URI.id, Uri.Builder() - .fromEntry(entry, spaEnvironment.sliceProviderAuthorities) - ) } private fun fetchStatusData(entry: SettingsEntry, cursor: MatrixCursor) { @@ -252,11 +245,6 @@ class SpaSearchProvider : ContentProvider() { .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name) .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras)) } - if (entry.hasSliceSupport) - row.add( - ColumnEnum.SLICE_URI.id, Uri.Builder() - .fromEntry(entry, spaEnvironment.sliceProviderAuthorities) - ) // Fetch status data. We can add runtime arguments later if necessary val statusData = entry.getStatusData() ?: return row.add(ColumnEnum.ENTRY_DISABLED.id, statusData.isDisabled) diff --git a/packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml b/packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml index 6552296bc4e4..bc5ec6911939 100644 --- a/packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml +++ b/packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml @@ -28,7 +28,7 @@ android:layout_height="match_parent" android:orientation="vertical"> - <com.android.settingslib.notification.ZenRadioLayout + <com.android.settingslib.notification.modes.ZenRadioLayout android:id="@+id/zen_duration_conditions" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -46,7 +46,7 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"/> - </com.android.settingslib.notification.ZenRadioLayout> + </com.android.settingslib.notification.modes.ZenRadioLayout> </LinearLayout> </ScrollView>
\ No newline at end of file diff --git a/packages/SettingsLib/res/values/colors.xml b/packages/SettingsLib/res/values/colors.xml index f89fe935df38..67139b510d85 100644 --- a/packages/SettingsLib/res/values/colors.xml +++ b/packages/SettingsLib/res/values/colors.xml @@ -14,8 +14,7 @@ limitations under the License. --> -<resources xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> +<resources> <color name="disabled_text_color">#66000000</color> <!-- 38% black --> <color name="bt_color_icon_1">#b4a50e0e</color> <!-- 72% Material Red 900 --> @@ -34,8 +33,8 @@ <color name="bt_color_bg_6">#e9d2fd</color> <!-- Material Purple 100 --> <color name="bt_color_bg_7">#cbf0f8</color> <!-- Material Cyan 100 --> - <color name="black">@*android:color/black</color> - <color name="white">@*android:color/white</color> + <color name="dark_mode_icon_color_single_tone">#99000000</color> + <color name="light_mode_icon_color_single_tone">#ffffff</color> <color name="user_avatar_color_bg">?android:attr/colorBackgroundFloating</color> diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java index 6c2bd412cfbd..ef0f6cbc6ed9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java +++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java @@ -100,9 +100,9 @@ public class SignalDrawable extends DrawableWrapper { mCutoutHeightFraction = context.getResources().getFloat( com.android.internal.R.dimen.config_signalCutoutHeightFraction); mDarkModeFillColor = Utils.getColorStateListDefaultColor(context, - R.color.black); + R.color.dark_mode_icon_color_single_tone); mLightModeFillColor = Utils.getColorStateListDefaultColor(context, - R.color.white); + R.color.light_mode_icon_color_single_tone); mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size); mTransparentPaint.setColor(context.getColor(android.R.color.transparent)); mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java index 6571dd7ba398..eced7b3a116b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java @@ -29,6 +29,7 @@ import android.media.session.MediaController; import android.os.UserHandle; import android.text.TextUtils; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -41,7 +42,6 @@ import java.util.HashMap; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -65,7 +65,9 @@ public final class RouterInfoMediaManager extends InfoMediaManager { refreshDevices(); }; - private final AtomicReference<MediaRouter2.ScanToken> mScanToken = new AtomicReference<>(); + @GuardedBy("this") + @Nullable + private MediaRouter2.ScanToken mScanToken; // TODO (b/321969740): Plumb target UserHandle between UMO and RouterInfoMediaManager. /* package */ RouterInfoMediaManager( @@ -101,8 +103,13 @@ public final class RouterInfoMediaManager extends InfoMediaManager { @Override protected void startScanOnRouter() { if (Flags.enableScreenOffScanning()) { - MediaRouter2.ScanRequest request = new MediaRouter2.ScanRequest.Builder().build(); - mScanToken.compareAndSet(null, mRouter.requestScan(request)); + synchronized (this) { + if (mScanToken == null) { + MediaRouter2.ScanRequest request = + new MediaRouter2.ScanRequest.Builder().build(); + mScanToken = mRouter.requestScan(request); + } + } } else { mRouter.startScan(); } @@ -120,9 +127,11 @@ public final class RouterInfoMediaManager extends InfoMediaManager { @Override protected void stopScanOnRouter() { if (Flags.enableScreenOffScanning()) { - MediaRouter2.ScanToken token = mScanToken.getAndSet(null); - if (token != null) { - mRouter.cancelScanRequest(token); + synchronized (this) { + if (mScanToken != null) { + mRouter.cancelScanRequest(mScanToken); + mScanToken = null; + } } } else { mRouter.stopScan(); diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java index 8868837e4cff..4028b73a2c71 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java @@ -105,12 +105,6 @@ public class DataServiceUtils { public static final String COLUMN_CARD_STATE = "cardState"; /** - * The name of the extended APDU supported state column, see - * {@link UiccSlotInfo#getIsExtendedApduSupported()}. - */ - public static final String COLUMN_IS_EXTENDED_APDU_SUPPORTED = "isExtendedApduSupported"; - - /** * The name of the removable state column, see {@link UiccSlotInfo#isRemovable()}. */ public static final String COLUMN_IS_REMOVABLE = "isRemovable"; @@ -150,74 +144,17 @@ public class DataServiceUtils { public static final String COLUMN_SIM_SLOT_INDEX = "simSlotIndex"; /** - * The name of the carrier ID column, see {@link SubscriptionInfo#getCarrierId()}. - */ - public static final String COLUMN_CARRIER_ID = "carrierId"; - - /** - * The name of the display name column, see {@link SubscriptionInfo#getDisplayName()}. - */ - public static final String COLUMN_DISPLAY_NAME = "displayName"; - - /** - * The name of the carrier name column, see {@link SubscriptionInfo#getCarrierName()}. - */ - public static final String COLUMN_CARRIER_NAME = "carrierName"; - - /** - * The name of the data roaming state column, see - * {@link SubscriptionInfo#getDataRoaming()}. - */ - public static final String COLUMN_DATA_ROAMING = "dataRoaming"; - - /** - * The name of the mcc column, see {@link SubscriptionInfo#getMccString()}. - */ - public static final String COLUMN_MCC = "mcc"; - - /** - * The name of the mnc column, see {@link SubscriptionInfo#getMncString()}. - */ - public static final String COLUMN_MNC = "mnc"; - - /** - * The name of the country ISO column, see {@link SubscriptionInfo#getCountryIso()}. - */ - public static final String COLUMN_COUNTRY_ISO = "countryIso"; - - /** * The name of the embedded state column, see {@link SubscriptionInfo#isEmbedded()}. */ public static final String COLUMN_IS_EMBEDDED = "isEmbedded"; /** - * The name of the card ID column, see {@link SubscriptionInfo#getCardId()}. - */ - public static final String COLUMN_CARD_ID = "cardId"; - - /** - * The name of the port index column, see {@link SubscriptionInfo#getPortIndex()}. - */ - public static final String COLUMN_PORT_INDEX = "portIndex"; - - /** * The name of the opportunistic state column, see * {@link SubscriptionInfo#isOpportunistic()}. */ public static final String COLUMN_IS_OPPORTUNISTIC = "isOpportunistic"; /** - * The name of the groupUUID column, see {@link SubscriptionInfo#getGroupUuid()}. - */ - public static final String COLUMN_GROUP_UUID = "groupUUID"; - - /** - * The name of the subscription type column, see - * {@link SubscriptionInfo#getSubscriptionType()}}. - */ - public static final String COLUMN_SUBSCRIPTION_TYPE = "subscriptionType"; - - /** * The name of the uniqueName column, * {@see SubscriptionUtil#getUniqueSubscriptionDisplayName(SubscriptionInfo, Context)}. */ @@ -231,19 +168,6 @@ public class DataServiceUtils { public static final String COLUMN_IS_SUBSCRIPTION_VISIBLE = "isSubscriptionVisible"; /** - * The name of the formatted phone number column, - * {@see SubscriptionUtil#getFormattedPhoneNumber(Context, SubscriptionInfo)}. - */ - public static final String COLUMN_FORMATTED_PHONE_NUMBER = "getFormattedPhoneNumber"; - - /** - * The name of the first removable subscription state column, - * {@see SubscriptionUtil#getFirstRemovableSubscription(Context)}. - */ - public static final String COLUMN_IS_FIRST_REMOVABLE_SUBSCRIPTION = - "isFirstRemovableSubscription"; - - /** * The name of the default subscription selection column, * {@see SubscriptionUtil#getSubscriptionOrDefault(Context, int)}. */ @@ -257,24 +181,12 @@ public class DataServiceUtils { public static final String COLUMN_IS_VALID_SUBSCRIPTION = "isValidSubscription"; /** - * The name of the usable subscription column, - * {@link SubscriptionManager#isUsableSubscriptionId(int)}. - */ - public static final String COLUMN_IS_USABLE_SUBSCRIPTION = "isUsableSubscription"; - - /** * The name of the active subscription column, * {@link SubscriptionManager#isActiveSubscriptionId(int)}. */ public static final String COLUMN_IS_ACTIVE_SUBSCRIPTION_ID = "isActiveSubscription"; /** - * The name of the available subscription column, - * {@see SubscriptionUtil#getAvailableSubscription(Context, ProxySubscriptionManager, int)}. - */ - public static final String COLUMN_IS_AVAILABLE_SUBSCRIPTION = "isAvailableSubscription"; - - /** * The name of the active data subscription state column, see * {@link SubscriptionManager#getActiveDataSubscriptionId()}. */ diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java index e6b1cfb440fa..060eab6681c6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java @@ -40,15 +40,6 @@ public interface SubscriptionInfoDao { + DataServiceUtils.SubscriptionInfoData.COLUMN_ID + " = :subId") SubscriptionInfoEntity querySubInfoById(String subId); - @Query("SELECT * FROM " + DataServiceUtils.SubscriptionInfoData.TABLE_NAME + " WHERE " - + DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_SUBSCRIPTION_ID - + " = :isActiveSubscription" + " AND " - + DataServiceUtils.SubscriptionInfoData.COLUMN_IS_SUBSCRIPTION_VISIBLE - + " = :isSubscriptionVisible" + " ORDER BY " - + DataServiceUtils.SubscriptionInfoData.COLUMN_SIM_SLOT_INDEX) - LiveData<List<SubscriptionInfoEntity>> queryActiveSubInfos( - boolean isActiveSubscription, boolean isSubscriptionVisible); - @Query("SELECT COUNT(*) FROM " + DataServiceUtils.SubscriptionInfoData.TABLE_NAME) int count(); diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java index 361a24612b3a..88e6a57bf45b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java @@ -19,7 +19,6 @@ package com.android.settingslib.mobile.dataservice; import android.text.TextUtils; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; @@ -28,39 +27,19 @@ import java.util.Objects; @Entity(tableName = DataServiceUtils.SubscriptionInfoData.TABLE_NAME) public class SubscriptionInfoEntity { - public SubscriptionInfoEntity(@NonNull String subId, int simSlotIndex, int carrierId, - String displayName, String carrierName, int dataRoaming, String mcc, String mnc, - String countryIso, boolean isEmbedded, int cardId, int portIndex, - boolean isOpportunistic, @Nullable String groupUUID, int subscriptionType, - String uniqueName, boolean isSubscriptionVisible, @Nullable String formattedPhoneNumber, - boolean isFirstRemovableSubscription, boolean isDefaultSubscriptionSelection, - boolean isValidSubscription, boolean isUsableSubscription, - boolean isActiveSubscriptionId, boolean isAvailableSubscription, - boolean isActiveDataSubscriptionId) { + public SubscriptionInfoEntity(@NonNull String subId, int simSlotIndex, boolean isEmbedded, + boolean isOpportunistic, String uniqueName, boolean isSubscriptionVisible, + boolean isDefaultSubscriptionSelection, boolean isValidSubscription, + boolean isActiveSubscriptionId, boolean isActiveDataSubscriptionId) { this.subId = subId; this.simSlotIndex = simSlotIndex; - this.carrierId = carrierId; - this.displayName = displayName; - this.carrierName = carrierName; - this.dataRoaming = dataRoaming; - this.mcc = mcc; - this.mnc = mnc; - this.countryIso = countryIso; this.isEmbedded = isEmbedded; - this.cardId = cardId; - this.portIndex = portIndex; this.isOpportunistic = isOpportunistic; - this.groupUUID = groupUUID; - this.subscriptionType = subscriptionType; this.uniqueName = uniqueName; this.isSubscriptionVisible = isSubscriptionVisible; - this.formattedPhoneNumber = formattedPhoneNumber; - this.isFirstRemovableSubscription = isFirstRemovableSubscription; this.isDefaultSubscriptionSelection = isDefaultSubscriptionSelection; this.isValidSubscription = isValidSubscription; - this.isUsableSubscription = isUsableSubscription; this.isActiveSubscriptionId = isActiveSubscriptionId; - this.isAvailableSubscription = isAvailableSubscription; this.isActiveDataSubscriptionId = isActiveDataSubscriptionId; } @@ -72,59 +51,18 @@ public class SubscriptionInfoEntity { @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_SIM_SLOT_INDEX) public int simSlotIndex; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_CARRIER_ID) - public int carrierId; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_DISPLAY_NAME) - public String displayName; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_CARRIER_NAME) - public String carrierName; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_DATA_ROAMING) - public int dataRoaming; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_MCC) - public String mcc; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_MNC) - public String mnc; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_COUNTRY_ISO) - public String countryIso; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_EMBEDDED) public boolean isEmbedded; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_CARD_ID) - public int cardId; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_PORT_INDEX) - public int portIndex; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_OPPORTUNISTIC) public boolean isOpportunistic; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_GROUP_UUID) - @Nullable - public String groupUUID; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_SUBSCRIPTION_TYPE) - public int subscriptionType; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_UNIQUE_NAME) public String uniqueName; @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_SUBSCRIPTION_VISIBLE) public boolean isSubscriptionVisible; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_FORMATTED_PHONE_NUMBER) - @Nullable - public String formattedPhoneNumber; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_FIRST_REMOVABLE_SUBSCRIPTION) - public boolean isFirstRemovableSubscription; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_DEFAULT_SUBSCRIPTION_SELECTION) public boolean isDefaultSubscriptionSelection; @@ -132,15 +70,9 @@ public class SubscriptionInfoEntity { @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_VALID_SUBSCRIPTION) public boolean isValidSubscription; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_USABLE_SUBSCRIPTION) - public boolean isUsableSubscription; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_SUBSCRIPTION_ID) public boolean isActiveSubscriptionId; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_AVAILABLE_SUBSCRIPTION) - public boolean isAvailableSubscription; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_DATA_SUBSCRIPTION) public boolean isActiveDataSubscriptionId; @@ -165,28 +97,13 @@ public class SubscriptionInfoEntity { return Objects.hash( subId, simSlotIndex, - carrierId, - displayName, - carrierName, - dataRoaming, - mcc, - mnc, - countryIso, isEmbedded, - cardId, - portIndex, isOpportunistic, - groupUUID, - subscriptionType, uniqueName, isSubscriptionVisible, - formattedPhoneNumber, - isFirstRemovableSubscription, isDefaultSubscriptionSelection, isValidSubscription, - isUsableSubscription, isActiveSubscriptionId, - isAvailableSubscription, isActiveDataSubscriptionId); } @@ -202,28 +119,13 @@ public class SubscriptionInfoEntity { SubscriptionInfoEntity info = (SubscriptionInfoEntity) obj; return TextUtils.equals(subId, info.subId) && simSlotIndex == info.simSlotIndex - && carrierId == info.carrierId - && TextUtils.equals(displayName, info.displayName) - && TextUtils.equals(carrierName, info.carrierName) - && dataRoaming == info.dataRoaming - && TextUtils.equals(mcc, info.mcc) - && TextUtils.equals(mnc, info.mnc) - && TextUtils.equals(countryIso, info.countryIso) && isEmbedded == info.isEmbedded - && cardId == info.cardId - && portIndex == info.portIndex && isOpportunistic == info.isOpportunistic - && TextUtils.equals(groupUUID, info.groupUUID) - && subscriptionType == info.subscriptionType && TextUtils.equals(uniqueName, info.uniqueName) && isSubscriptionVisible == info.isSubscriptionVisible - && TextUtils.equals(formattedPhoneNumber, info.formattedPhoneNumber) - && isFirstRemovableSubscription == info.isFirstRemovableSubscription && isDefaultSubscriptionSelection == info.isDefaultSubscriptionSelection && isValidSubscription == info.isValidSubscription - && isUsableSubscription == info.isUsableSubscription && isActiveSubscriptionId == info.isActiveSubscriptionId - && isAvailableSubscription == info.isAvailableSubscription && isActiveDataSubscriptionId == info.isActiveDataSubscriptionId; } @@ -233,50 +135,20 @@ public class SubscriptionInfoEntity { .append(subId) .append(", simSlotIndex = ") .append(simSlotIndex) - .append(", carrierId = ") - .append(carrierId) - .append(", displayName = ") - .append(displayName) - .append(", carrierName = ") - .append(carrierName) - .append(", dataRoaming = ") - .append(dataRoaming) - .append(", mcc = ") - .append(mcc) - .append(", mnc = ") - .append(mnc) - .append(", countryIso = ") - .append(countryIso) .append(", isEmbedded = ") .append(isEmbedded) - .append(", cardId = ") - .append(cardId) - .append(", portIndex = ") - .append(portIndex) .append(", isOpportunistic = ") .append(isOpportunistic) - .append(", groupUUID = ") - .append(groupUUID) - .append(", subscriptionType = ") - .append(subscriptionType) .append(", uniqueName = ") .append(uniqueName) .append(", isSubscriptionVisible = ") .append(isSubscriptionVisible) - .append(", formattedPhoneNumber = ") - .append(formattedPhoneNumber) - .append(", isFirstRemovableSubscription = ") - .append(isFirstRemovableSubscription) .append(", isDefaultSubscriptionSelection = ") .append(isDefaultSubscriptionSelection) .append(", isValidSubscription = ") .append(isValidSubscription) - .append(", isUsableSubscription = ") - .append(isUsableSubscription) .append(", isActiveSubscriptionId = ") .append(isActiveSubscriptionId) - .append(", isAvailableSubscription = ") - .append(isAvailableSubscription) .append(", isActiveDataSubscriptionId = ") .append(isActiveDataSubscriptionId) .append(")}"); diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt index 273a63db801c..72c3c1719f70 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt @@ -56,7 +56,7 @@ class ZenModeRepositoryImpl( val backgroundHandler: Handler?, ) : ZenModeRepository { - private val notificationBroadcasts = + private val notificationBroadcasts by lazy { callbackFlow { val receiver = object : BroadcastReceiver() { @@ -95,8 +95,9 @@ class ZenModeRepositoryImpl( ) } } + } - override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> = + override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy { if (Flags.volumePanelBroadcastFix() && android.app.Flags.modesApi()) flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) { notificationManager.consolidatedNotificationPolicy @@ -105,11 +106,13 @@ class ZenModeRepositoryImpl( flowFromBroadcast(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) { notificationManager.consolidatedNotificationPolicy } + } - override val globalZenMode: StateFlow<Int?> = + override val globalZenMode: StateFlow<Int?> by lazy { flowFromBroadcast(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED) { notificationManager.zenMode } + } private fun <T> flowFromBroadcast(intentAction: String, mapper: () -> T) = notificationBroadcasts diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java index 2cc911234404..03a2b7526f62 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java @@ -286,11 +286,7 @@ public class ZenMode implements Parcelable { mRule, oldCondition, conditionId)); } - public boolean canEditName() { - return !isManualDnd(); - } - - public boolean canEditIcon() { + public boolean canEditNameAndIcon() { return !isManualDnd(); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt index 096c25db8629..06333b61eeb1 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt @@ -48,7 +48,6 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @SmallTest class ZenModeRepositoryTest { - @Mock private lateinit var context: Context @Mock private lateinit var notificationManager: NotificationManager @@ -73,7 +72,7 @@ class ZenModeRepositoryTest { ) } - @DisableFlags(android.app.Flags.FLAG_MODES_API, Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX) + @DisableFlags(Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX) @Test fun consolidatedPolicyChanges_repositoryEmits_flagsOff() { testScope.runTest { @@ -88,9 +87,7 @@ class ZenModeRepositoryTest { triggerIntent(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) runCurrent() - assertThat(values) - .containsExactlyElementsIn(listOf(null, testPolicy1, testPolicy2)) - .inOrder() + assertThat(values).containsExactly(null, testPolicy1, testPolicy2).inOrder() } } @@ -109,9 +106,7 @@ class ZenModeRepositoryTest { triggerIntent(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) runCurrent() - assertThat(values) - .containsExactlyElementsIn(listOf(null, testPolicy1, testPolicy2)) - .inOrder() + assertThat(values).containsExactly(null, testPolicy1, testPolicy2).inOrder() } } @@ -128,14 +123,13 @@ class ZenModeRepositoryTest { runCurrent() assertThat(values) - .containsExactlyElementsIn( - listOf(null, Global.ZEN_MODE_OFF, Global.ZEN_MODE_ALARMS)) + .containsExactly(null, Global.ZEN_MODE_OFF, Global.ZEN_MODE_ALARMS) .inOrder() } } private fun triggerIntent(action: String) { - verify(context).registerReceiver(receiverCaptor.capture(), any()) + verify(context).registerReceiver(receiverCaptor.capture(), any(), any(), any()) receiverCaptor.value.onReceive(context, Intent(action)) } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java index 9e543122e707..e705f97202a3 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java @@ -56,12 +56,14 @@ public class ZenModeTest { assertThat(zenMode.getId()).isEqualTo("id"); assertThat(zenMode.getRule()).isEqualTo(ZEN_RULE); assertThat(zenMode.isManualDnd()).isFalse(); + assertThat(zenMode.canEditNameAndIcon()).isTrue(); assertThat(zenMode.canBeDeleted()).isTrue(); assertThat(zenMode.isActive()).isTrue(); ZenMode manualMode = ZenMode.manualDndMode(ZEN_RULE, false); assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID); assertThat(manualMode.isManualDnd()).isTrue(); + assertThat(manualMode.canEditNameAndIcon()).isFalse(); assertThat(manualMode.canBeDeleted()).isFalse(); assertThat(manualMode.isActive()).isFalse(); } diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 9cbb1bd9c71c..db71d72752ce 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -669,6 +669,7 @@ android_library { ], asset_dirs: [ "tests/goldens", + "schemas", ], static_libs: [ "SystemUI-res", @@ -708,6 +709,7 @@ android_library { "androidx-constraintlayout_constraintlayout", "androidx.exifinterface_exifinterface", "androidx.room_room-runtime", + "androidx.room_room-testing", "androidx.room_room-ktx", "androidx.datastore_datastore-preferences", "device_state_flags_lib", @@ -789,7 +791,11 @@ android_library { "android.test.mock", "keepanno-annotations", ], - kotlincflags: ["-Xjvm-default=all"], + kotlincflags: [ + "-Xjvm-default=all", + // TODO(b/352363800): Why do we need this? + "-J-Xmx8192M", + ], aaptflags: [ "--extra-packages", "com.android.systemui", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index e2ecda320065..b580eb1dfef6 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -591,6 +591,16 @@ flag { } flag { + name: "screenshot_save_image_exporter" + namespace: "systemui" + description: "Save all screenshots using ImageExporter" + bug: "352308052" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "run_fingerprint_detect_on_dismissible_keyguard" namespace: "systemui" description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible." @@ -1143,3 +1153,30 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "systemui" + name: "qs_register_setting_observer_on_bg_thread" + description: "Registers Quick Settings content providers on background thread" + bug: "351766769" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "sounddose_customization" + namespace: "systemui" + description: "Enables custom actions for sounddose notifications" + bug: "345227709" +} + +flag { + namespace: "systemui" + name: "register_content_observers_async" + description: "Use new Async API to register content observers" + bug: "316922634" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt index b8f9ca82f072..f655ac1d207b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt @@ -83,6 +83,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.PlatformButton import com.android.compose.animation.Easings import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.SceneTransitionLayout @@ -516,13 +517,22 @@ private fun FoldAware( val currentSceneKey = if (isSplitAroundTheFold) SceneKeys.SplitSceneKey else SceneKeys.ContiguousSceneKey - SceneTransitionLayout( - currentScene = currentSceneKey, - onChangeScene = {}, - transitions = SceneTransitions, - modifier = modifier, - enableInterruptions = false, - ) { + val state = remember { + MutableSceneTransitionLayoutState( + currentSceneKey, + SceneTransitions, + enableInterruptions = false, + ) + } + + // Update state whenever currentSceneKey has changed. + LaunchedEffect(state, currentSceneKey) { + if (currentSceneKey != state.transitionState.currentScene) { + state.setTargetScene(currentSceneKey, coroutineScope = this) + } + } + + SceneTransitionLayout(state, modifier = modifier) { scene(SceneKeys.ContiguousSceneKey) { FoldableScene( aboveFold = aboveFold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index 0a4c6fd21922..7400711e36df 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -67,8 +67,7 @@ constructor( @Composable override fun SceneScope.Content(modifier: Modifier) { val isUdfpsVisible = viewModel.isUdfpsVisible - val shouldUseSplitNotificationShade by - viewModel.shouldUseSplitNotificationShade.collectAsStateWithLifecycle() + val isShadeLayoutWide by viewModel.isShadeLayoutWide.collectAsStateWithLifecycle() val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle() LockscreenLongPress( @@ -100,7 +99,7 @@ constructor( } ) } - if (shouldUseSplitNotificationShade) { + if (isShadeLayoutWide) { with(notificationSection) { Notifications( burnInParams = null, @@ -112,7 +111,7 @@ constructor( } } } - if (!shouldUseSplitNotificationShade) { + if (!isShadeLayoutWide) { with(notificationSection) { Notifications( burnInParams = null, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt index 065f2a2172b1..72cf83269760 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt @@ -69,8 +69,7 @@ constructor( @Composable override fun SceneScope.Content(modifier: Modifier) { val isUdfpsVisible = viewModel.isUdfpsVisible - val shouldUseSplitNotificationShade by - viewModel.shouldUseSplitNotificationShade.collectAsStateWithLifecycle() + val isShadeLayoutWide by viewModel.isShadeLayoutWide.collectAsStateWithLifecycle() val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle() LockscreenLongPress( @@ -102,7 +101,7 @@ constructor( }, ) } - if (shouldUseSplitNotificationShade) { + if (isShadeLayoutWide) { with(notificationSection) { Notifications( burnInParams = null, @@ -114,7 +113,7 @@ constructor( } } } - if (!shouldUseSplitNotificationShade) { + if (!isShadeLayoutWide) { with(notificationSection) { Notifications( burnInParams = null, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 7f80dfa703bc..a1f20423ad3b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -90,8 +90,8 @@ constructor( */ @Composable fun SceneScope.Notifications(burnInParams: BurnInParameters?, modifier: Modifier = Modifier) { - val shouldUseSplitNotificationShade by - lockscreenContentViewModel.shouldUseSplitNotificationShade.collectAsStateWithLifecycle() + val isShadeLayoutWide by + lockscreenContentViewModel.isShadeLayoutWide.collectAsStateWithLifecycle() val areNotificationsVisible by lockscreenContentViewModel.areNotificationsVisible.collectAsStateWithLifecycle() val splitShadeTopMargin: Dp = @@ -111,9 +111,7 @@ constructor( modifier = modifier .fillMaxWidth() - .thenIf(shouldUseSplitNotificationShade) { - Modifier.padding(top = splitShadeTopMargin) - } + .thenIf(isShadeLayoutWide) { Modifier.padding(top = splitShadeTopMargin) } .let { if (burnInParams == null) { it diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt index 166aa70c1d4a..f9e22525237b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.height import com.android.keyguard.dagger.KeyguardStatusBarViewComponent @@ -83,29 +82,20 @@ constructor( componentFactory.build(view, provider).keyguardStatusBarViewController } - MovableElement( - key = StatusBarElementKey, - modifier = modifier, - ) { - content { - AndroidView( - factory = { - notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let { - (it.parent as ViewGroup).removeView(it) - } + AndroidView( + factory = { + notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let { + (it.parent as ViewGroup).removeView(it) + } - viewController.init() - view - }, - modifier = - Modifier.fillMaxWidth().padding(horizontal = 16.dp).height { - Utils.getStatusBarHeaderHeightKeyguard(context) - }, - update = { viewController.setDisplayCutout(viewDisplayCutout) } - ) - } - } + viewController.init() + view + }, + modifier = + Modifier.fillMaxWidth().padding(horizontal = 16.dp).height { + Utils.getStatusBarHeaderHeightKeyguard(context) + }, + update = { viewController.setDisplayCutout(viewDisplayCutout) } + ) } } - -private val StatusBarElementKey = ElementKey("StatusBar") diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index 067315381773..0cd4b6816a61 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -33,6 +34,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.SceneTransitionLayout import com.android.compose.modifiers.thenIf @@ -78,13 +80,22 @@ constructor( WeatherClockScenes.splitShadeLargeClockScene } - SceneTransitionLayout( - modifier = modifier, - currentScene = currentScene, - onChangeScene = {}, - transitions = ClockTransition.defaultClockTransitions, - enableInterruptions = false, - ) { + val state = remember { + MutableSceneTransitionLayoutState( + currentScene, + ClockTransition.defaultClockTransitions, + enableInterruptions = false, + ) + } + + // Update state whenever currentSceneKey has changed. + LaunchedEffect(state, currentScene) { + if (currentScene != state.transitionState.currentScene) { + state.setTargetScene(currentScene, coroutineScope = this) + } + } + + SceneTransitionLayout(state, modifier) { scene(splitShadeLargeClockScene) { LargeClockWithSmartSpace( shouldOffSetClockToOneHalf = !hasCustomPositionUpdatedAnimation diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt index 581f3a5cacff..d629eec1c45a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt @@ -16,8 +16,10 @@ package com.android.systemui.media.controls.ui.composable +import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable @@ -26,7 +28,6 @@ import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.view.contains import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.systemui.media.controls.ui.controller.MediaCarouselController @@ -36,7 +37,8 @@ import com.android.systemui.util.animation.MeasurementInput private object MediaCarousel { object Elements { - internal val Content = ElementKey("MediaCarouselContent") + internal val Content = + ElementKey(debugName = "MediaCarouselContent", scenePicker = MediaScenePicker) } } @@ -61,40 +63,43 @@ fun SceneScope.MediaCarousel( mediaHost.measurementInput = MeasurementInput(layoutWidth, layoutHeight) carouselController.setSceneContainerSize(layoutWidth, layoutHeight) - AndroidView( - modifier = - modifier - .element(MediaCarousel.Elements.Content) - .height(mediaHeight) - .fillMaxWidth() - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) + MovableElement( + key = MediaCarousel.Elements.Content, + modifier = modifier.height(mediaHeight).fillMaxWidth() + ) { + content { + AndroidView( + modifier = + Modifier.fillMaxSize().layout { measurable, constraints -> + val placeable = measurable.measure(constraints) - // Notify controller to size the carousel for the current space - mediaHost.measurementInput = MeasurementInput(placeable.width, placeable.height) - carouselController.setSceneContainerSize(placeable.width, placeable.height) + // Notify controller to size the carousel for the current space + mediaHost.measurementInput = + MeasurementInput(placeable.width, placeable.height) + carouselController.setSceneContainerSize(placeable.width, placeable.height) - layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } + layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } + }, + factory = { context -> + FrameLayout(context).apply { + layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + ) + } }, - factory = { context -> - FrameLayout(context).apply { - val mediaFrame = carouselController.mediaFrame - (mediaFrame.parent as? ViewGroup)?.removeView(mediaFrame) - addView(mediaFrame) - layoutParams = - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT, - ) - } - }, - update = { - if (it.contains(carouselController.mediaFrame)) { - return@AndroidView - } - val mediaFrame = carouselController.mediaFrame - (mediaFrame.parent as? ViewGroup)?.removeView(mediaFrame) - it.addView(mediaFrame) - }, - ) + update = { it.setView(carouselController.mediaFrame) }, + onRelease = { it.removeAllViews() } + ) + } + } +} + +private fun ViewGroup.setView(view: View) { + if (view.parent == this) { + return + } + (view.parent as? ViewGroup)?.removeView(view) + addView(view) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt new file mode 100644 index 000000000000..039813390f1e --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt @@ -0,0 +1,72 @@ +/* + * 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.media.controls.ui.composable + +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.ElementScenePicker +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.TransitionState +import com.android.systemui.scene.shared.model.Scenes + +/** [ElementScenePicker] implementation for the media carousel object. */ +object MediaScenePicker : ElementScenePicker { + + private val shadeLockscreenFraction = 0.65f + private val scenes = + setOf( + Scenes.Lockscreen, + Scenes.Shade, + Scenes.QuickSettings, + Scenes.QuickSettingsShade, + Scenes.Communal + ) + + override fun sceneDuringTransition( + element: ElementKey, + transition: TransitionState.Transition, + fromSceneZIndex: Float, + toSceneZIndex: Float + ): SceneKey? { + return when { + // TODO: 352052894 - update with the actual scene picking + transition.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) -> { + if (transition.progress < shadeLockscreenFraction) { + Scenes.Lockscreen + } else { + Scenes.Shade + } + } + + // TODO: 345467290 - update with the actual scene picking + transition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) -> { + if (transition.progress < 1f - shadeLockscreenFraction) { + Scenes.Shade + } else { + Scenes.Lockscreen + } + } + + // TODO: 345467290 - update with the actual scene picking + transition.isTransitioningBetween(Scenes.QuickSettings, Scenes.Shade) -> { + Scenes.QuickSettings + } + + // TODO: 340216785 - update with the actual scene picking + else -> pickSingleSceneIn(scenes, transition, element) + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 4a6599a04fab..805351ea8bbe 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -34,9 +34,11 @@ import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -51,6 +53,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity @@ -60,6 +63,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexScenePicker +import com.android.compose.animation.scene.NestedScrollBehavior import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.TransitionState import com.android.compose.animation.scene.UserAction @@ -255,6 +259,8 @@ private fun SceneScope.SingleShade( val mediaOffset by animateSceneDpAsState(value = InQQS, key = MediaLandscapeTopOffset, canOverflow = false) + val navBarHeight = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() + Box( modifier = modifier.thenIf(shouldPunchHoleBehindScrim) { @@ -358,11 +364,22 @@ private fun SceneScope.SingleShade( notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt()) } } - NotificationStackCutoffGuideline( - stackScrollView = notificationStackScrollView, - viewModel = notificationsPlaceholderViewModel, - modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding() - ) + Box( + modifier = + Modifier.align(Alignment.BottomCenter) + .height(navBarHeight) + .pointerInteropFilter { true } + .verticalNestedScrollToScene( + topBehavior = NestedScrollBehavior.EdgeAlways, + isExternalOverscrollGesture = { false } + ) + ) { + NotificationStackCutoffGuideline( + stackScrollView = notificationStackScrollView, + viewModel = notificationsPlaceholderViewModel, + modifier = Modifier.align(Alignment.TopCenter) + ) + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/ui/composable/StatusBar.kt b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/ui/composable/StatusBar.kt deleted file mode 100644 index f514ab4a710b..000000000000 --- a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/ui/composable/StatusBar.kt +++ /dev/null @@ -1,43 +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.statusbar.ui.composable - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun StatusBar( - modifier: Modifier = Modifier, -) { - // TODO(b/272780101): implement. - Row( - modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp).padding(4.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Text("Status bar") - } -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index cdcfc84ce92a..615d393f8bee 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -241,43 +241,50 @@ internal class MultiPointerDraggableNode( } } - awaitPointerEventScope { - while (isActive) { - try { - detectDragGestures( - orientation = orientation, - startDragImmediately = startDragImmediately, - onDragStart = { startedPosition, overSlop, pointersDown -> - velocityTracker.resetTracking() - onDragStarted(startedPosition, overSlop, pointersDown) - }, - onDrag = { controller, change, amount -> - velocityTracker.addPointerInputChange(change) - controller.onDrag(amount) - }, - onDragEnd = { controller -> - val viewConfiguration = currentValueOf(LocalViewConfiguration) - val maxVelocity = - viewConfiguration.maximumFlingVelocity.let { Velocity(it, it) } - val velocity = velocityTracker.calculateVelocity(maxVelocity) - controller.onStop( - velocity = - when (orientation) { - Orientation.Horizontal -> velocity.x - Orientation.Vertical -> velocity.y - }, - canChangeScene = true, - ) - }, - onDragCancel = { controller -> - controller.onStop(velocity = 0f, canChangeScene = true) - }, - swipeDetector = swipeDetector - ) - } catch (exception: CancellationException) { - // If the coroutine scope is active, we can just restart the drag cycle. - if (!isActive) { - throw exception + // The order is important here: we want to make sure that the previous PointerEventScope + // is initialized first. This ensures that the following PointerEventScope doesn't + // receive more events than the first one. + launch { + awaitPointerEventScope { + while (isActive) { + try { + detectDragGestures( + orientation = orientation, + startDragImmediately = startDragImmediately, + onDragStart = { startedPosition, overSlop, pointersDown -> + velocityTracker.resetTracking() + onDragStarted(startedPosition, overSlop, pointersDown) + }, + onDrag = { controller, change, amount -> + velocityTracker.addPointerInputChange(change) + controller.onDrag(amount) + }, + onDragEnd = { controller -> + val viewConfiguration = currentValueOf(LocalViewConfiguration) + val maxVelocity = + viewConfiguration.maximumFlingVelocity.let { + Velocity(it, it) + } + val velocity = velocityTracker.calculateVelocity(maxVelocity) + controller.onStop( + velocity = + when (orientation) { + Orientation.Horizontal -> velocity.x + Orientation.Vertical -> velocity.y + }, + canChangeScene = true, + ) + }, + onDragCancel = { controller -> + controller.onStop(velocity = 0f, canChangeScene = true) + }, + swipeDetector = swipeDetector + ) + } catch (exception: CancellationException) { + // If the coroutine scope is active, we can just restart the drag cycle. + if (!isActive) { + throw exception + } } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt index dfb8c499b4c9..734241e2faf6 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt @@ -56,13 +56,7 @@ internal fun PredictiveBackHandler( progress.collect { backEvent -> transition.dragProgress = backEvent.progress } // Back gesture successful. - transition.animateTo( - if (state.canChangeScene(targetSceneForBack)) { - targetSceneForBack - } else { - fromScene - } - ) + transition.animateTo(targetSceneForBack) } catch (e: CancellationException) { // Back gesture cancelled. transition.animateTo(fromScene) @@ -105,12 +99,15 @@ private class PredictiveBackTransition( return it } - currentScene = scene + if (scene != currentScene && state.transitionState == this && state.canChangeScene(scene)) { + currentScene = scene + } + val targetProgress = - when (scene) { + when (currentScene) { fromScene -> 0f toScene -> 1f - else -> error("scene $scene should be either $fromScene or $toScene") + else -> error("scene $currentScene should be either $fromScene or $toScene") } val animatable = Animatable(dragProgress).also { progressAnimatable = it } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt deleted file mode 100644 index b346a70e61f9..000000000000 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PunchHole.kt +++ /dev/null @@ -1,178 +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.compose.animation.scene - -import androidx.compose.runtime.Stable -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.Outline -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.drawOutline -import androidx.compose.ui.graphics.drawscope.ContentDrawScope -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.translate -import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.Measurable -import androidx.compose.ui.layout.MeasureResult -import androidx.compose.ui.layout.MeasureScope -import androidx.compose.ui.node.DelegatingNode -import androidx.compose.ui.node.DrawModifierNode -import androidx.compose.ui.node.GlobalPositionAwareModifierNode -import androidx.compose.ui.node.LayoutModifierNode -import androidx.compose.ui.node.ModifierNodeElement -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.toSize - -/** - * Punch a hole in this node with the given [size], [offset] and [shape]. - * - * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area. - * This can be used to make content drawn below an opaque element visible. For example, if we have - * [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below - * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big clock - * time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be the - * result. - */ -@Stable -fun Modifier.punchHole( - size: () -> Size, - offset: () -> Offset, - shape: Shape = RectangleShape, -): Modifier = this.then(PunchHoleElement(size, offset, shape)) - -/** - * Punch a hole in this node using the bounds of [coords] and the given [shape]. - * - * You can use [androidx.compose.ui.layout.onGloballyPositioned] to get the last coordinates of a - * node. - */ -@Stable -fun Modifier.punchHole( - coords: () -> LayoutCoordinates?, - shape: Shape = RectangleShape, -): Modifier = this.then(PunchHoleWithBoundsElement(coords, shape)) - -private data class PunchHoleElement( - private val size: () -> Size, - private val offset: () -> Offset, - private val shape: Shape, -) : ModifierNodeElement<PunchHoleNode>() { - override fun create(): PunchHoleNode = PunchHoleNode(size, offset, { shape }) - - override fun update(node: PunchHoleNode) { - node.size = size - node.offset = offset - node.shape = { shape } - } -} - -private class PunchHoleNode( - var size: () -> Size, - var offset: () -> Offset, - var shape: () -> Shape, -) : Modifier.Node(), DrawModifierNode, LayoutModifierNode { - private var lastSize: Size = Size.Unspecified - private var lastLayoutDirection: LayoutDirection = LayoutDirection.Ltr - private var lastOutline: Outline? = null - - override fun MeasureScope.measure( - measurable: Measurable, - constraints: Constraints - ): MeasureResult { - return measurable.measure(constraints).run { - layout(width, height) { - placeWithLayer(0, 0) { compositingStrategy = CompositingStrategy.Offscreen } - } - } - } - - override fun ContentDrawScope.draw() { - drawContent() - - val holeSize = size() - if (holeSize != Size.Zero) { - val offset = offset() - translate(offset.x, offset.y) { drawHole(holeSize) } - } - } - - private fun DrawScope.drawHole(size: Size) { - if (shape == RectangleShape) { - drawRect(Color.Black, size = size, blendMode = BlendMode.DstOut) - return - } - - val outline = - if (size == lastSize && layoutDirection == lastLayoutDirection) { - lastOutline!! - } else { - val newOutline = shape().createOutline(size, layoutDirection, this) - lastSize = size - lastLayoutDirection = layoutDirection - lastOutline = newOutline - newOutline - } - - drawOutline( - outline, - Color.Black, - blendMode = BlendMode.DstOut, - ) - } -} - -private data class PunchHoleWithBoundsElement( - private val coords: () -> LayoutCoordinates?, - private val shape: Shape, -) : ModifierNodeElement<PunchHoleWithBoundsNode>() { - override fun create(): PunchHoleWithBoundsNode = PunchHoleWithBoundsNode(coords, shape) - - override fun update(node: PunchHoleWithBoundsNode) { - node.holeCoords = coords - node.shape = shape - } -} - -private class PunchHoleWithBoundsNode( - var holeCoords: () -> LayoutCoordinates?, - var shape: Shape, -) : DelegatingNode(), DrawModifierNode, GlobalPositionAwareModifierNode { - private val delegate = delegate(PunchHoleNode(::holeSize, ::holeOffset, ::shape)) - private var lastCoords: LayoutCoordinates? = null - - override fun onGloballyPositioned(coordinates: LayoutCoordinates) { - this.lastCoords = coordinates - } - - override fun ContentDrawScope.draw() = with(delegate) { draw() } - - private fun holeSize(): Size { - return holeCoords()?.size?.toSize() ?: Size.Zero - } - - private fun holeOffset(): Offset { - val holeCoords = holeCoords() ?: return Offset.Zero - val lastCoords = lastCoords ?: error("draw() was called before onGloballyPositioned()") - return lastCoords.localPositionOf(holeCoords, relativeToSource = Offset.Zero) - } -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 7c8fce8f297d..45758c53d69a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -48,7 +48,6 @@ import androidx.compose.ui.unit.IntSize * @param transitionInterceptionThreshold used during a scene transition. For the scene to be * intercepted, the progress value must be above the threshold, and below (1 - threshold). * @param scenes the configuration of the different scenes of this layout. - * @see updateSceneTransitionLayoutState */ @Composable fun SceneTransitionLayout( @@ -70,56 +69,6 @@ fun SceneTransitionLayout( ) } -/** - * [SceneTransitionLayout] is a container that automatically animates its content whenever - * [currentScene] changes, using the transitions defined in [transitions]. - * - * Note: You should use [androidx.compose.animation.AnimatedContent] instead of - * [SceneTransitionLayout] if it fits your need. Use [SceneTransitionLayout] over AnimatedContent if - * you need support for swipe gestures, shared elements or transitions defined declaratively outside - * UI code. - * - * @param currentScene the current scene - * @param onChangeScene a mutator that should set [currentScene] to the given scene when called. - * This is called when the user commits a transition to a new scene because of a [UserAction], for - * instance by triggering back navigation or by swiping to a new scene. - * @param transitions the definition of the transitions used to animate a change of scene. - * @param swipeSourceDetector the source detector used to detect which source a swipe is started - * from, if any. - * @param transitionInterceptionThreshold used during a scene transition. For the scene to be - * intercepted, the progress value must be above the threshold, and below (1 - threshold). - * @param scenes the configuration of the different scenes of this layout. - */ -@Composable -fun SceneTransitionLayout( - currentScene: SceneKey, - onChangeScene: (SceneKey) -> Unit, - transitions: SceneTransitions, - modifier: Modifier = Modifier, - swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, - swipeDetector: SwipeDetector = DefaultSwipeDetector, - @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, - enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, - scenes: SceneTransitionLayoutScope.() -> Unit, -) { - val state = - updateSceneTransitionLayoutState( - currentScene, - onChangeScene, - transitions, - enableInterruptions = enableInterruptions, - ) - - SceneTransitionLayout( - state, - modifier, - swipeSourceDetector, - swipeDetector, - transitionInterceptionThreshold, - scenes, - ) -} - interface SceneTransitionLayoutScope { /** * Add a scene to this layout, identified by [key]. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 5b4fbf036083..56c8752eb53c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -22,13 +22,9 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.Orientation -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastFilter @@ -38,14 +34,12 @@ import com.android.compose.animation.scene.transition.link.StateLink import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch /** * The state of a [SceneTransitionLayout]. * * @see MutableSceneTransitionLayoutState - * @see updateSceneTransitionLayoutState */ @Stable sealed interface SceneTransitionLayoutState { @@ -152,55 +146,6 @@ fun MutableSceneTransitionLayoutState( ) } -/** - * Sets up a [SceneTransitionLayoutState] and keeps it synced with [currentScene], [onChangeScene] - * and [transitions]. New transitions will automatically be started whenever [currentScene] is - * changed. - * - * @param currentScene the current scene - * @param onChangeScene a mutator that should set [currentScene] to the given scene when called. - * This is called when the user commits a transition to a new scene because of a [UserAction], for - * instance by triggering back navigation or by swiping to a new scene. - * @param transitions the definition of the transitions used to animate a change of scene. - * @param canChangeScene whether we can transition to the given scene. This is called when the user - * commits a transition to a new scene because of a [UserAction]. If [canChangeScene] returns - * `true`, then [onChangeScene] will be called right afterwards with the same [SceneKey]. If it - * returns `false`, the user action will be cancelled and we will animate back to the current - * scene. - * @param stateLinks the [StateLink] connecting this [SceneTransitionLayoutState] to other - * [SceneTransitionLayoutState]s. - */ -@Composable -fun updateSceneTransitionLayoutState( - currentScene: SceneKey, - onChangeScene: (SceneKey) -> Unit, - transitions: SceneTransitions = SceneTransitions.Empty, - canChangeScene: (SceneKey) -> Boolean = { true }, - stateLinks: List<StateLink> = emptyList(), - enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, -): SceneTransitionLayoutState { - return remember { - HoistedSceneTransitionLayoutState( - currentScene, - transitions, - onChangeScene, - canChangeScene, - stateLinks, - enableInterruptions, - ) - } - .apply { - update( - currentScene, - onChangeScene, - canChangeScene, - transitions, - stateLinks, - enableInterruptions, - ) - } -} - @Stable sealed interface TransitionState { /** @@ -729,58 +674,6 @@ internal abstract class BaseSceneTransitionLayoutState( } } -/** - * A [SceneTransitionLayout] whose current scene/source of truth is hoisted (its current value comes - * from outside). - */ -internal class HoistedSceneTransitionLayoutState( - initialScene: SceneKey, - override var transitions: SceneTransitions, - private var changeScene: (SceneKey) -> Unit, - private var canChangeScene: (SceneKey) -> Boolean, - stateLinks: List<StateLink> = emptyList(), - enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, -) : BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) { - private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED) - - override fun canChangeScene(scene: SceneKey): Boolean = canChangeScene.invoke(scene) - - override fun CoroutineScope.onChangeScene(scene: SceneKey) = changeScene.invoke(scene) - - @Composable - fun update( - currentScene: SceneKey, - onChangeScene: (SceneKey) -> Unit, - canChangeScene: (SceneKey) -> Boolean, - transitions: SceneTransitions, - stateLinks: List<StateLink>, - enableInterruptions: Boolean, - ) { - SideEffect { - this.changeScene = onChangeScene - this.canChangeScene = canChangeScene - this.transitions = transitions - this.stateLinks = stateLinks - this.enableInterruptions = enableInterruptions - - targetSceneChannel.trySend(currentScene) - } - - LaunchedEffect(targetSceneChannel) { - for (newKey in targetSceneChannel) { - // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame - // late. - val newKey = targetSceneChannel.tryReceive().getOrNull() ?: newKey - animateToScene( - layoutState = this@HoistedSceneTransitionLayoutState, - target = newKey, - transitionKey = null, - ) - } - } - } -} - /** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */ internal class MutableSceneTransitionLayoutStateImpl( initialScene: SceneKey, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 1ae999282afa..7988e0e4e416 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -203,26 +203,28 @@ class ElementTest { val elementSize = 50.dp val elementOffset = 20.dp - lateinit var changeScene: (SceneKey) -> Unit - - rule.testTransition( - from = SceneA, - to = SceneB, - transitionLayout = { currentScene, onChangeScene -> - changeScene = onChangeScene - - SceneTransitionLayout( - currentScene, - onChangeScene, + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, transitions { from(SceneA, to = SceneB) { spec = tween } from(SceneB, to = SceneC) { spec = tween } }, - // Disable interruptions so that the current transition is directly removed when - // starting a new one. + // Disable interruptions so that the current transition is directly removed + // when starting a new one. enableInterruptions = false, - ) { + ) + } + + lateinit var coroutineScope: CoroutineScope + rule.testTransition( + state = state, + to = SceneB, + transitionLayout = { state -> + coroutineScope = rememberCoroutineScope() + SceneTransitionLayout(state) { scene(SceneA) { Box(Modifier.size(layoutSize)) { // Transformed element @@ -243,7 +245,7 @@ class ElementTest { onElement(TestElements.Bar).assertExists() // Start transition from SceneB to SceneC - changeScene(SceneC) + rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) } } at(3 * frameDuration) { onElement(TestElements.Bar).assertIsNotDisplayed() } @@ -340,18 +342,16 @@ class ElementTest { @Test fun elementIsReusedBetweenScenes() { - var currentScene by mutableStateOf(SceneA) + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } var sceneCState by mutableStateOf(0) val key = TestElements.Foo var nullableLayoutImpl: SceneTransitionLayoutImpl? = null + lateinit var coroutineScope: CoroutineScope rule.setContent { + coroutineScope = rememberCoroutineScope() SceneTransitionLayoutForTesting( - state = - updateSceneTransitionLayoutState( - currentScene = currentScene, - onChangeScene = { currentScene = it } - ), + state = state, onLayoutImpl = { nullableLayoutImpl = it }, ) { scene(SceneA) { /* Nothing */ } @@ -375,7 +375,7 @@ class ElementTest { assertThat(layoutImpl.elements).isEmpty() // Scene B: element is in the map. - currentScene = SceneB + rule.runOnUiThread { state.setTargetScene(SceneB, coroutineScope) } rule.waitForIdle() assertThat(layoutImpl.elements.keys).containsExactly(key) @@ -383,7 +383,7 @@ class ElementTest { assertThat(element.sceneStates.keys).containsExactly(SceneB) // Scene C, state 0: the same element is reused. - currentScene = SceneC + rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) } sceneCState = 0 rule.waitForIdle() @@ -472,12 +472,13 @@ class ElementTest { @Test fun elementModifierSupportsUpdates() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } var key by mutableStateOf(TestElements.Foo) var nullableLayoutImpl: SceneTransitionLayoutImpl? = null rule.setContent { SceneTransitionLayoutForTesting( - state = updateSceneTransitionLayoutState(currentScene = SceneA, onChangeScene = {}), + state = state, onLayoutImpl = { nullableLayoutImpl = it }, ) { scene(SceneA) { Box(Modifier.element(key)) } @@ -521,11 +522,12 @@ class ElementTest { rule.waitUntil(timeoutMillis = 10_000) { animationFinished } } + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } rule.setContent { scrollScope = rememberCoroutineScope() SceneTransitionLayoutForTesting( - state = updateSceneTransitionLayoutState(currentScene = SceneA, onChangeScene = {}), + state = state, onLayoutImpl = { nullableLayoutImpl = it }, ) { scene(SceneA) { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt index 55431354b693..f717301dba38 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt @@ -40,7 +40,13 @@ class ObservableTransitionStateTest { @Test fun testObservableTransitionState() = runTest { - lateinit var state: SceneTransitionLayoutState + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, + EmptyTestTransitions, + ) + } // Collect the current observable state into [observableState]. // TODO(b/290184746): Use collectValues {} once it is extracted into a library that can be @@ -63,16 +69,9 @@ class ObservableTransitionStateTest { } rule.testTransition( - from = SceneA, + state = state, to = SceneB, - transitionLayout = { currentScene, onChangeScene -> - state = - updateSceneTransitionLayoutState( - currentScene, - onChangeScene, - EmptyTestTransitions - ) - + transitionLayout = { SceneTransitionLayout(state = state) { scene(SceneA) {} scene(SceneB) {} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt new file mode 100644 index 000000000000..6522eb33bd49 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt @@ -0,0 +1,159 @@ +/* + * 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.compose.animation.scene + +import androidx.activity.BackEventCompat +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestScenes.SceneA +import com.android.compose.animation.scene.TestScenes.SceneB +import com.android.compose.animation.scene.TestScenes.SceneC +import com.android.compose.animation.scene.subjects.assertThat +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PredictiveBackHandlerTest { + // We use createAndroidComposeRule() here and not createComposeRule() because we need an + // activity for testBack(). + @get:Rule val rule = createAndroidComposeRule<ComponentActivity>() + + @Test + fun testBack() { + val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + rule.setContent { + SceneTransitionLayout(layoutState) { + scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) + + rule.runOnUiThread { rule.activity.onBackPressedDispatcher.onBackPressed() } + rule.waitForIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneB) + } + + @Test + fun testPredictiveBack() { + val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + rule.setContent { + SceneTransitionLayout(layoutState) { + scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) + + // Start back. + val dispatcher = rule.activity.onBackPressedDispatcher + rule.runOnUiThread { + dispatcher.dispatchOnBackStarted(backEvent()) + dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f)) + } + + val transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + assertThat(transition).hasProgress(0.4f) + + // Cancel it. + rule.runOnUiThread { dispatcher.dispatchOnBackCancelled() } + rule.waitForIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) + assertThat(layoutState.transitionState).isIdle() + + // Start again and commit it. + rule.runOnUiThread { + dispatcher.dispatchOnBackStarted(backEvent()) + dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f)) + dispatcher.onBackPressed() + } + rule.waitForIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneB) + assertThat(layoutState.transitionState).isIdle() + } + + @Test + fun interruptedPredictiveBackDoesNotCallCanChangeScene() { + var canChangeSceneCalled = false + val layoutState = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, + canChangeScene = { + canChangeSceneCalled = true + true + }, + ) + } + + lateinit var coroutineScope: CoroutineScope + rule.setContent { + coroutineScope = rememberCoroutineScope() + SceneTransitionLayout(layoutState) { + scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + scene(SceneC) { Box(Modifier.fillMaxSize()) } + } + } + + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) + + // Start back. + val dispatcher = rule.activity.onBackPressedDispatcher + rule.runOnUiThread { dispatcher.dispatchOnBackStarted(backEvent()) } + + val predictiveTransition = assertThat(layoutState.transitionState).isTransition() + assertThat(predictiveTransition).hasFromScene(SceneA) + assertThat(predictiveTransition).hasToScene(SceneB) + + // Start a new transition to C. + rule.runOnUiThread { layoutState.setTargetScene(SceneC, coroutineScope) } + val newTransition = assertThat(layoutState.transitionState).isTransition() + assertThat(newTransition).hasFromScene(SceneA) + assertThat(newTransition).hasToScene(SceneC) + + // Commit the back gesture. It shouldn't call canChangeScene given that the back transition + // was interrupted. + rule.runOnUiThread { dispatcher.onBackPressed() } + rule.waitForIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneC) + assertThat(layoutState.transitionState).isIdle() + assertThat(predictiveTransition).hasCurrentScene(SceneA) + assertThat(canChangeSceneCalled).isFalse() + } + + private fun backEvent(progress: Float = 0f): BackEventCompat { + return BackEventCompat( + touchX = 0f, + touchY = 0f, + progress = progress, + swipeEdge = BackEventCompat.EDGE_LEFT, + ) + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index 1c8efb82fd24..1ec10793363e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -16,8 +16,6 @@ package com.android.compose.animation.scene -import androidx.activity.BackEventCompat -import androidx.activity.ComponentActivity import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween @@ -31,6 +29,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,7 +41,7 @@ import androidx.compose.ui.test.assertHeightIsEqualTo import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertPositionInRootIsEqualTo import androidx.compose.ui.test.assertWidthIsEqualTo -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onChild import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText @@ -58,6 +58,7 @@ import com.android.compose.test.assertSizeIsEqualTo import com.android.compose.test.subjects.DpOffsetSubject import com.android.compose.test.subjects.assertThat import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test @@ -69,23 +70,27 @@ class SceneTransitionLayoutTest { private val LayoutSize = 300.dp } - private var currentScene by mutableStateOf(SceneA) - private lateinit var layoutState: SceneTransitionLayoutState + private lateinit var coroutineScope: CoroutineScope + private lateinit var layoutState: MutableSceneTransitionLayoutState + private var currentScene: SceneKey + get() = layoutState.transitionState.currentScene + set(value) { + rule.runOnUiThread { layoutState.setTargetScene(value, coroutineScope) } + } - // We use createAndroidComposeRule() here and not createComposeRule() because we need an - // activity for testBack(). - @get:Rule val rule = createAndroidComposeRule<ComponentActivity>() + @get:Rule val rule = createComposeRule() /** The content under test. */ @Composable private fun TestContent(enableInterruptions: Boolean = true) { - layoutState = - updateSceneTransitionLayoutState( - currentScene, - { currentScene = it }, + coroutineScope = rememberCoroutineScope() + layoutState = remember { + MutableSceneTransitionLayoutState( + SceneA, EmptyTestTransitions, enableInterruptions = enableInterruptions, ) + } SceneTransitionLayout( state = layoutState, @@ -164,52 +169,6 @@ class SceneTransitionLayoutTest { } @Test - fun testBack() { - rule.setContent { TestContent() } - - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - - rule.runOnUiThread { rule.activity.onBackPressedDispatcher.onBackPressed() } - rule.waitForIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneB) - } - - @Test - fun testPredictiveBack() { - rule.setContent { TestContent() } - - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - - // Start back. - val dispatcher = rule.activity.onBackPressedDispatcher - rule.runOnUiThread { - dispatcher.dispatchOnBackStarted(backEvent()) - dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f)) - } - - val transition = assertThat(layoutState.transitionState).isTransition() - assertThat(transition).hasFromScene(SceneA) - assertThat(transition).hasToScene(SceneB) - assertThat(transition).hasProgress(0.4f) - - // Cancel it. - rule.runOnUiThread { dispatcher.dispatchOnBackCancelled() } - rule.waitForIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - assertThat(layoutState.transitionState).isIdle() - - // Start again and commit it. - rule.runOnUiThread { - dispatcher.dispatchOnBackStarted(backEvent()) - dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f)) - dispatcher.onBackPressed() - } - rule.waitForIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneB) - assertThat(layoutState.transitionState).isIdle() - } - - @Test fun testTransitionState() { rule.setContent { TestContent() } assertThat(layoutState.transitionState).isIdle() @@ -218,23 +177,15 @@ class SceneTransitionLayoutTest { // We will advance the clock manually. rule.mainClock.autoAdvance = false - // Change the current scene. Until composition is triggered, this won't change the layout - // state. + // Change the current scene. currentScene = SceneB - assertThat(layoutState.transitionState).isIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - - // On the next frame, we will recompose because currentScene changed, which will start the - // transition (i.e. it will change the transitionState to be a Transition) in a - // LaunchedEffect. - rule.mainClock.advanceTimeByFrame() val transition = assertThat(layoutState.transitionState).isTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneB) assertThat(transition).hasProgress(0f) // Then, on the next frame, the animator we started gets its initial value and clock - // starting time. We are now at progress = 0f. + // starting time. We are still at progress = 0f. rule.mainClock.advanceTimeByFrame() assertThat(transition).hasProgress(0f) @@ -275,12 +226,9 @@ class SceneTransitionLayoutTest { // Pause animations to test the state mid-transition. rule.mainClock.autoAdvance = false - // Go to scene B and let the animation start. See [testLayoutState()] and - // [androidx.compose.ui.test.MainTestClock] to understand why we need to advance the clock - // by 2 frames to be at the start of the animation. + // Go to scene B and let the animation start. currentScene = SceneB rule.mainClock.advanceTimeByFrame() - rule.mainClock.advanceTimeByFrame() // Advance to the middle of the animation. rule.mainClock.advanceTimeBy(TestTransitionDuration / 2) @@ -311,7 +259,6 @@ class SceneTransitionLayoutTest { // Animate to scene C, let the animation start then go to the middle of the transition. currentScene = SceneC rule.mainClock.advanceTimeByFrame() - rule.mainClock.advanceTimeByFrame() rule.mainClock.advanceTimeBy(TestTransitionDuration / 2) // In Scene C, foo is at the bottom start of the layout and has a size of 150.dp. The @@ -409,24 +356,24 @@ class SceneTransitionLayoutTest { fun multipleTransitionsWillComposeMultipleScenes() { val duration = 10 * 16L - var currentScene: SceneKey by mutableStateOf(SceneA) - lateinit var state: SceneTransitionLayoutState - rule.setContent { - state = - updateSceneTransitionLayoutState( - currentScene = currentScene, - onChangeScene = { currentScene = it }, - transitions = - transitions { - from(SceneA, to = SceneB) { - spec = tween(duration.toInt(), easing = LinearEasing) - } - from(SceneB, to = SceneC) { - spec = tween(duration.toInt(), easing = LinearEasing) - } + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, + transitions { + from(SceneA, to = SceneB) { + spec = tween(duration.toInt(), easing = LinearEasing) } + from(SceneB, to = SceneC) { + spec = tween(duration.toInt(), easing = LinearEasing) + } + } ) + } + lateinit var coroutineScope: CoroutineScope + rule.setContent { + coroutineScope = rememberCoroutineScope() SceneTransitionLayout(state) { scene(SceneA) { Box(Modifier.testTag("aRoot").fillMaxSize()) } scene(SceneB) { Box(Modifier.testTag("bRoot").fillMaxSize()) } @@ -444,12 +391,11 @@ class SceneTransitionLayoutTest { rule.mainClock.autoAdvance = false // Start A => B and go to the middle of the transition. - currentScene = SceneB + rule.runOnUiThread { state.setTargetScene(SceneB, coroutineScope) } - // We need to tick 2 frames after changing [currentScene] before the animation actually + // We need to tick 1 frames after changing [currentScene] before the animation actually // starts. rule.mainClock.advanceTimeByFrame() - rule.mainClock.advanceTimeByFrame() rule.mainClock.advanceTimeBy(duration / 2) rule.waitForIdle() @@ -462,8 +408,7 @@ class SceneTransitionLayoutTest { rule.onNodeWithTag("cRoot").assertDoesNotExist() // Start B => C. - currentScene = SceneC - rule.mainClock.advanceTimeByFrame() + rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) } rule.mainClock.advanceTimeByFrame() rule.waitForIdle() @@ -517,12 +462,7 @@ class SceneTransitionLayoutTest { assertThrows(IllegalStateException::class.java) { rule.setContent { SceneTransitionLayout( - state = - updateSceneTransitionLayoutState( - currentScene = currentScene, - onChangeScene = { currentScene = it }, - transitions = EmptyTestTransitions - ), + state = remember { MutableSceneTransitionLayoutState(SceneA) }, modifier = Modifier.size(LayoutSize), ) { // from SceneA to SceneA @@ -560,13 +500,4 @@ class SceneTransitionLayoutTest { assertThat(keyInB).isEqualTo(SceneB) assertThat(keyInC).isEqualTo(SceneC) } - - private fun backEvent(progress: Float = 0f): BackEventCompat { - return BackEventCompat( - touchX = 0f, - touchY = 0f, - progress = progress, - swipeEdge = BackEventCompat.EDGE_LEFT, - ) - } } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt index de46f7209c84..fbd557f3cbb3 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt @@ -27,12 +27,6 @@ fun TestSceneScope( content: @Composable SceneScope.() -> Unit, ) { val currentScene = remember { SceneKey("current") } - SceneTransitionLayout( - currentScene, - onChangeScene = { /* do nothing */}, - transitions = remember { transitions {} }, - modifier, - ) { - scene(currentScene, content = content) - } + val state = remember { MutableSceneTransitionLayoutState(currentScene) } + SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) } } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt index 6724851dbec5..a37d78ef8a71 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt @@ -19,13 +19,14 @@ package com.android.compose.animation.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.junit4.ComposeContentTestRule +import kotlinx.coroutines.CoroutineScope import platform.test.motion.MotionTestRule import platform.test.motion.RecordedMotion import platform.test.motion.compose.ComposeRecordingSpec @@ -95,20 +96,24 @@ fun ComposeContentTestRule.testTransition( builder: TransitionTestBuilder.() -> Unit, ) { testTransition( - from = fromScene, + state = + runOnUiThread { + MutableSceneTransitionLayoutState( + fromScene, + transitions { from(fromScene, to = toScene, builder = transition) } + ) + }, to = toScene, - transitionLayout = { currentScene, onChangeScene -> + transitionLayout = { state -> SceneTransitionLayout( - currentScene, - onChangeScene, - transitions { from(fromScene, to = toScene, builder = transition) }, + state, layoutModifier, ) { scene(fromScene, content = fromSceneContent) scene(toScene, content = toSceneContent) } }, - builder, + builder = builder, ) } @@ -172,21 +177,19 @@ fun MotionTestRule<ComposeToolkit>.recordTransition( ) } -/** - * Test the transition between two scenes of [transitionLayout][SceneTransitionLayout] at different - * points in time. - */ +/** Test the transition from [state] to [to]. */ fun ComposeContentTestRule.testTransition( - from: SceneKey, + state: MutableSceneTransitionLayoutState, to: SceneKey, - transitionLayout: - @Composable - ( - currentScene: SceneKey, - onChangeScene: (SceneKey) -> Unit, - ) -> Unit, + transitionLayout: @Composable (state: MutableSceneTransitionLayoutState) -> Unit, builder: TransitionTestBuilder.() -> Unit, ) { + val currentScene = state.transitionState.currentScene + check(currentScene != to) { + "The 'to' scene (${to.debugName}) should be different from the state current scene " + + "(${currentScene.debugName})" + } + val test = transitionTest(builder) val assertionScope = object : TransitionTestAssertionScope { @@ -198,8 +201,11 @@ fun ComposeContentTestRule.testTransition( } } - var currentScene by mutableStateOf(from) - setContent { transitionLayout(currentScene, { currentScene = it }) } + lateinit var coroutineScope: CoroutineScope + setContent { + coroutineScope = rememberCoroutineScope() + transitionLayout(state) + } // Wait for the UI to be idle then test the before state. waitForIdle() @@ -209,14 +215,8 @@ fun ComposeContentTestRule.testTransition( mainClock.autoAdvance = false // Change the current scene. - currentScene = to - - // Advance by a frame to trigger recomposition, which will start the transition (i.e. it will - // change the transitionState to be a Transition) in a LaunchedEffect. - mainClock.advanceTimeByFrame() - - // Advance by another frame so that the animator we started gets its initial value and clock - // starting time. We are now at progress = 0f. + runOnUiThread { state.setTargetScene(to, coroutineScope) } + waitForIdle() mainClock.advanceTimeByFrame() waitForIdle() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt index 722eb2b9b622..60aea92ab3bd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt @@ -20,6 +20,9 @@ import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent import android.content.mockedContext +import android.os.Handler +import android.os.fakeExecutorHandler +import android.provider.Settings.Secure.USER_SETUP_COMPLETE import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -27,11 +30,13 @@ import com.android.systemui.broadcast.FakeBroadcastDispatcher import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.widgets.CommunalWidgetModule +import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before @@ -41,6 +46,8 @@ import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor @SmallTest @RunWith(AndroidJUnit4::class) @@ -50,10 +57,13 @@ class CommunalBackupRestoreStartableTest : SysuiTestCase() { @Mock private lateinit var communalInteractor: CommunalInteractor - private val mapCaptor = kotlinArgumentCaptor<Map<Int, Int>>() + private val mapCaptor = argumentCaptor<Map<Int, Int>>() private lateinit var context: Context private lateinit var broadcastDispatcher: FakeBroadcastDispatcher + private lateinit var secureSettings: SecureSettings + private lateinit var handler: Handler + private lateinit var fakeExecutor: FakeExecutor private lateinit var underTest: CommunalBackupRestoreStartable @Before @@ -62,18 +72,28 @@ class CommunalBackupRestoreStartableTest : SysuiTestCase() { context = kosmos.mockedContext broadcastDispatcher = kosmos.broadcastDispatcher + secureSettings = kosmos.fakeSettings + handler = kosmos.fakeExecutorHandler + fakeExecutor = kosmos.fakeExecutor + + secureSettings.putInt(USER_SETUP_COMPLETE, 0) underTest = CommunalBackupRestoreStartable( broadcastDispatcher, communalInteractor, logcatLogBuffer("CommunalBackupRestoreStartable"), + secureSettings, + handler, ) } @Test - fun testRestoreWidgetsUponHostRestored() = + fun restoreWidgets_userSetUpComplete_performRestore() = testScope.runTest { + // User set up complete + secureSettings.putInt(USER_SETUP_COMPLETE, 1) + underTest.start() // Verify restore widgets not called @@ -94,7 +114,7 @@ class CommunalBackupRestoreStartableTest : SysuiTestCase() { // Verify restore widgets called verify(communalInteractor).restoreWidgets(mapCaptor.capture()) - val oldToNewWidgetIdMap = mapCaptor.value + val oldToNewWidgetIdMap = mapCaptor.firstValue assertThat(oldToNewWidgetIdMap) .containsExactlyEntriesIn( mapOf( @@ -106,10 +126,54 @@ class CommunalBackupRestoreStartableTest : SysuiTestCase() { } @Test - fun testDoNotRestoreWidgetsIfNotForCommunalWidgetHost() = + fun restoreWidgets_userSetUpNotComplete_restoreWhenUserSetupComplete() = testScope.runTest { underTest.start() + // Verify restore widgets not called + verify(communalInteractor, never()).restoreWidgets(any()) + + // Trigger app widget host restored + val intent = + Intent().apply { + action = AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED + putExtra( + AppWidgetManager.EXTRA_HOST_ID, + CommunalWidgetModule.APP_WIDGET_HOST_ID + ) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS, intArrayOf(1, 2, 3)) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(7, 8, 9)) + } + broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + // Verify restore widgets not called because user setup not complete + verify(communalInteractor, never()).restoreWidgets(any()) + + // User setup complete + secureSettings.putInt(USER_SETUP_COMPLETE, 1) + fakeExecutor.runAllReady() + + // Verify restore widgets called + verify(communalInteractor).restoreWidgets(mapCaptor.capture()) + val oldToNewWidgetIdMap = mapCaptor.firstValue + assertThat(oldToNewWidgetIdMap) + .containsExactlyEntriesIn( + mapOf( + Pair(1, 7), + Pair(2, 8), + Pair(3, 9), + ) + ) + } + + @Test + fun restoreWidgets_broadcastNotForCommunalWidgetHost_doNotPerformRestore() = + testScope.runTest { + // User set up complete + secureSettings.putInt(USER_SETUP_COMPLETE, 1) + + underTest.start() + // Trigger app widget host restored, but for another host val hostId = CommunalWidgetModule.APP_WIDGET_HOST_ID + 1 val intent = @@ -126,8 +190,11 @@ class CommunalBackupRestoreStartableTest : SysuiTestCase() { } @Test - fun testAbortRestoreWidgetsIfOldToNewIdsMappingInvalid() = + fun restoreWidgets_oldToNewIdsMappingInvalid_abortRestore() = testScope.runTest { + // User set up complete + secureSettings.putInt(USER_SETUP_COMPLETE, 1) + underTest.start() // Trigger app widget host restored, but new ids list is one too many for old ids diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt new file mode 100644 index 000000000000..2f949335d42a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.setCommunalEnabled +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@EnableFlags(FLAG_COMMUNAL_HUB) +@RunWith(AndroidJUnit4::class) +class CommunalOngoingContentStartableTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val mediaRepository = kosmos.fakeCommunalMediaRepository + private val smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository + private val featureFlags = + kosmos.fakeFeatureFlagsClassic.apply { set(Flags.COMMUNAL_SERVICE_ENABLED, true) } + + private lateinit var underTest: CommunalOngoingContentStartable + + @Before + fun setUp() { + underTest = + CommunalOngoingContentStartable( + bgScope = kosmos.applicationCoroutineScope, + communalInteractor = kosmos.communalInteractor, + communalMediaRepository = mediaRepository, + communalSmartspaceRepository = smartspaceRepository, + featureFlags = featureFlags, + ) + } + + @Test + fun testListenForOngoingContentWhenCommunalIsEnabled() = + testScope.runTest { + underTest.start() + runCurrent() + + assertThat(mediaRepository.isListening()).isFalse() + assertThat(smartspaceRepository.isListening()).isFalse() + + kosmos.setCommunalEnabled(true) + runCurrent() + + assertThat(mediaRepository.isListening()).isTrue() + assertThat(smartspaceRepository.isListening()).isTrue() + + kosmos.setCommunalEnabled(false) + runCurrent() + + assertThat(mediaRepository.isListening()).isFalse() + assertThat(smartspaceRepository.isListening()).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt index cf145471e55f..0de036988337 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt @@ -27,6 +27,9 @@ import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dock.dockManager import com.android.systemui.dock.fakeDockManager +import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.flags.featureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor @@ -66,6 +69,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { fun setUp() { with(kosmos) { fakeSettings.putInt(Settings.System.SCREEN_OFF_TIMEOUT, SCREEN_TIMEOUT) + kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) underTest = CommunalSceneStartable( @@ -76,6 +80,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { keyguardInteractor = keyguardInteractor, systemSettings = fakeSettings, notificationShadeWindowController = notificationShadeWindowController, + featureFlagsClassic = kosmos.fakeFeatureFlagsClassic, applicationScope = applicationCoroutineScope, bgScope = applicationCoroutineScope, mainDispatcher = testDispatcher, @@ -451,6 +456,24 @@ class CommunalSceneStartableTest : SysuiTestCase() { } } + @Test + fun transitionFromDozingToGlanceableHub_forcesCommunal() = + with(kosmos) { + testScope.runTest { + val scene by collectLastValue(communalSceneInteractor.currentScene) + communalSceneInteractor.changeScene(CommunalScenes.Blank) + assertThat(scene).isEqualTo(CommunalScenes.Blank) + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.DOZING, + to = KeyguardState.GLANCEABLE_HUB, + testScope = this + ) + + assertThat(scene).isEqualTo(CommunalScenes.Communal) + } + } + private fun TestScope.updateDocked(docked: Boolean) = with(kosmos) { runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt index 407bf4cac633..dd280223f203 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt @@ -20,46 +20,41 @@ 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.kosmos.testScope import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData -import com.android.systemui.util.mockito.KotlinArgumentCaptor +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @android.platform.test.annotations.EnabledOnRavenwood class CommunalMediaRepositoryImplTest : SysuiTestCase() { - @Mock private lateinit var mediaDataManager: MediaDataManager - @Mock private lateinit var mediaData: MediaData - @Mock private lateinit var tableLogBuffer: TableLogBuffer + private val mediaDataManager = mock<MediaDataManager>() + private val mediaData = mock<MediaData>() + private val tableLogBuffer = mock<TableLogBuffer>() private lateinit var underTest: CommunalMediaRepositoryImpl - private val mediaDataListenerCaptor: KotlinArgumentCaptor<MediaDataManager.Listener> by lazy { - KotlinArgumentCaptor(MediaDataManager.Listener::class.java) - } + private val mediaDataListenerCaptor = argumentCaptor<MediaDataManager.Listener>() - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope @Before fun setUp() { - MockitoAnnotations.initMocks(this) - underTest = CommunalMediaRepositoryImpl( mediaDataManager, @@ -78,6 +73,8 @@ class CommunalMediaRepositoryImplTest : SysuiTestCase() { @Test fun mediaModel_updatesWhenMediaDataLoaded() = testScope.runTest { + underTest.startListening() + // Listener is added verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture()) @@ -89,7 +86,7 @@ class CommunalMediaRepositoryImplTest : SysuiTestCase() { // Change to media available and notify the listener. whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) whenever(mediaData.createdTimestampMillis).thenReturn(1234L) - mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData) + mediaDataListenerCaptor.firstValue.onMediaDataLoaded("key", null, mediaData) runCurrent() // Media active now returns true. @@ -100,12 +97,14 @@ class CommunalMediaRepositoryImplTest : SysuiTestCase() { @Test fun mediaModel_updatesWhenMediaDataRemoved() = testScope.runTest { + underTest.startListening() + // Listener is added verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture()) // Change to media available and notify the listener. whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) - mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData) + mediaDataListenerCaptor.firstValue.onMediaDataLoaded("key", null, mediaData) runCurrent() // Media active now returns true. @@ -114,7 +113,7 @@ class CommunalMediaRepositoryImplTest : SysuiTestCase() { // Change to media unavailable and notify the listener. whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false) - mediaDataListenerCaptor.value.onMediaDataRemoved("key", false) + mediaDataListenerCaptor.firstValue.onMediaDataRemoved("key", false) runCurrent() // Media active now returns false. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt new file mode 100644 index 000000000000..c1816ed3e5bf --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import android.app.smartspace.SmartspaceTarget +import android.app.smartspace.flags.Flags.FLAG_REMOTE_VIEWS +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.smartspace.CommunalSmartspaceController +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener +import com.android.systemui.testKosmos +import com.android.systemui.util.time.fakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class CommunalSmartspaceRepositoryImplTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val listenerCaptor = argumentCaptor<SmartspaceTargetListener>() + + private val smartspaceController = mock<CommunalSmartspaceController>() + private val fakeExecutor = kosmos.fakeExecutor + private val systemClock = kosmos.fakeSystemClock + + private lateinit var underTest: CommunalSmartspaceRepositoryImpl + + @Before + fun setUp() { + underTest = + CommunalSmartspaceRepositoryImpl( + smartspaceController, + fakeExecutor, + systemClock, + ) + } + + @DisableFlags(FLAG_REMOTE_VIEWS) + @Test + fun startListening_remoteViewsFlagDisabled_doNotListenForSmartspaceUpdates() = + testScope.runTest { + underTest.startListening() + fakeExecutor.runAllReady() + + verify(smartspaceController, never()).addListener(any()) + } + + @EnableFlags(FLAG_REMOTE_VIEWS) + @Test + fun startListening_remoteViewsFlagEnabled_listenForSmartspaceUpdates() = + testScope.runTest { + underTest.startListening() + fakeExecutor.runAllReady() + + // Verify listener added + val listener = captureSmartspaceTargetListener() + + underTest.stopListening() + fakeExecutor.runAllReady() + + // Verify listener removed + verify(smartspaceController).removeListener(listener) + } + + @EnableFlags(FLAG_REMOTE_VIEWS) + @Test + fun communalTimers_onlyShowTimersWithRemoteViews() = + testScope.runTest { + underTest.startListening() + + val communalTimers by collectLastValue(underTest.timers) + runCurrent() + fakeExecutor.runAllReady() + + with(captureSmartspaceTargetListener()) { + onSmartspaceTargetsUpdated( + listOf( + // Invalid. Not a timer + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("weather") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_WEATHER) + }, + // Invalid. RemoteViews absent + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-0-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(null) + on { creationTimeMillis }.doReturn(1000) + }, + // Valid + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-1-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(mock()) + on { creationTimeMillis }.doReturn(2000) + }, + ) + ) + } + runCurrent() + + // Verify that only the valid target is listed + assertThat(communalTimers).hasSize(1) + assertThat(communalTimers?.first()?.smartspaceTargetId).isEqualTo("timer-1-started") + } + + @EnableFlags(FLAG_REMOTE_VIEWS) + @Test + fun communalTimers_cacheCreationTime() = + testScope.runTest { + underTest.startListening() + + val communalTimers by collectLastValue(underTest.timers) + runCurrent() + fakeExecutor.runAllReady() + + val listener = captureSmartspaceTargetListener() + listener.onSmartspaceTargetsUpdated( + listOf( + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-0-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(mock()) + on { creationTimeMillis }.doReturn(1000) + }, + ) + ) + runCurrent() + + // Verify that the creation time is the current time, not the creation time passed in + // the target, because this value can be inaccurate (due to b/318535930). + val currentTime = systemClock.currentTimeMillis() + assertThat(communalTimers?.get(0)?.createdTimestampMillis).isEqualTo(currentTime) + assertThat(communalTimers?.get(0)?.createdTimestampMillis).isNotEqualTo(1000) + + // A second timer is added. + listener.onSmartspaceTargetsUpdated( + listOf( + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-0-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(mock()) + on { creationTimeMillis }.doReturn(2000) + }, + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-1-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(mock()) + on { creationTimeMillis }.doReturn(3000) + }, + ) + ) + runCurrent() + + // Verify that the created timestamp for the first time is consistent + assertThat(communalTimers?.get(0)?.createdTimestampMillis).isEqualTo(currentTime) + + // Verify that the second timer has a new creation time + assertThat(communalTimers?.get(1)?.createdTimestampMillis) + .isEqualTo(systemClock.currentTimeMillis()) + } + + @EnableFlags(FLAG_REMOTE_VIEWS) + @Test + fun communalTimers_creationTimeRemovedFromCacheWhenTimerRemoved() = + testScope.runTest { + underTest.startListening() + + val communalTimers by collectLastValue(underTest.timers) + runCurrent() + fakeExecutor.runAllReady() + + // Start timer 0 + val listener = captureSmartspaceTargetListener() + listener.onSmartspaceTargetsUpdated( + listOf( + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-0-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(mock()) + on { creationTimeMillis }.doReturn(1000) + }, + ) + ) + runCurrent() + + // Verify timer 0 creation time + val expectedCreationTimeForTimer0 = systemClock.currentTimeMillis() + assertThat(communalTimers?.first()?.createdTimestampMillis) + .isEqualTo(expectedCreationTimeForTimer0) + + // Advance some time + systemClock.advanceTime(1000) + + // Start timer 1 + listener.onSmartspaceTargetsUpdated( + listOf( + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-0-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(mock()) + on { creationTimeMillis }.doReturn(1000) + }, + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-1-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(mock()) + on { creationTimeMillis }.doReturn(2000) + }, + ) + ) + runCurrent() + + // Verify timer 1 creation time is new + val expectedCreationTimeForTimer1 = expectedCreationTimeForTimer0 + 1000 + assertThat(communalTimers?.get(1)?.createdTimestampMillis) + .isEqualTo(expectedCreationTimeForTimer1) + + // Removed timer 0 + listener.onSmartspaceTargetsUpdated( + listOf( + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-1-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(mock()) + on { creationTimeMillis }.doReturn(2000) + }, + ) + ) + runCurrent() + + // Verify timer 0 removed, and timer 1 creation time is correct + assertThat(communalTimers).hasSize(1) + assertThat(communalTimers?.first()?.createdTimestampMillis) + .isEqualTo(expectedCreationTimeForTimer1) + + // Advance some time + systemClock.advanceTime(1000) + + // Start timer 0 again. Technically this is a new timer, but timers can reused stable + // ids. + listener.onSmartspaceTargetsUpdated( + listOf( + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-1-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(mock()) + on { creationTimeMillis }.doReturn(2000) + }, + mock<SmartspaceTarget> { + on { smartspaceTargetId }.doReturn("timer-0-started") + on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER) + on { remoteViews }.doReturn(mock()) + on { creationTimeMillis }.doReturn(3000) + }, + ) + ) + runCurrent() + + // Verify new timer added, and timer 1 creation time is still correct + assertThat(communalTimers).hasSize(2) + assertThat(communalTimers?.get(0)?.createdTimestampMillis) + .isEqualTo(expectedCreationTimeForTimer1) + + // Verify creation time for the new timer is new, meaning that cache for timer 0 was + // removed when it was removed + assertThat(communalTimers?.get(1)?.createdTimestampMillis) + .isEqualTo(expectedCreationTimeForTimer1 + 1000) + } + + @Test + fun stableId() { + assertThat(CommunalSmartspaceRepositoryImpl.stableId("timer-0-12345-started")) + .isEqualTo("timer-0") + assertThat(CommunalSmartspaceRepositoryImpl.stableId("timer-1-67890-paused")) + .isEqualTo("timer-1") + assertThat(CommunalSmartspaceRepositoryImpl.stableId("i_am_an_unexpected_id")) + .isEqualTo("i_am_an_unexpected_id") + } + + private fun captureSmartspaceTargetListener(): SmartspaceTargetListener { + verify(smartspaceController).addListener(listenerCaptor.capture()) + return listenerCaptor.firstValue + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index 6ce6cdb32a12..17234a9067da 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -24,6 +24,7 @@ import android.content.ComponentName import android.content.applicationContext import android.graphics.Bitmap import android.os.UserHandle +import android.os.userManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -47,10 +48,6 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.logcatLogBuffer import com.android.systemui.res.R import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -59,11 +56,16 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.eq import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -77,6 +79,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var communalWidgetDao: CommunalWidgetDao @Mock private lateinit var backupManager: BackupManager + private val communalHubStateCaptor = argumentCaptor<CommunalHubState>() + private val componentNameCaptor = argumentCaptor<ComponentName>() + private lateinit var backupUtils: CommunalBackupUtils private lateinit var logBuffer: LogBuffer private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>> @@ -85,6 +90,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val packageChangeRepository = kosmos.fakePackageChangeRepository + private val userManager = kosmos.userManager + + private val mainUser = UserHandle(0) + private val workProfile = UserHandle(10) private val fakeAllowlist = listOf( @@ -109,6 +118,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets) whenever(communalWidgetHost.appWidgetProviders).thenReturn(fakeProviders) + whenever(userManager.mainUser).thenReturn(mainUser) + + restoreUser(mainUser) underTest = CommunalWidgetRepositoryImpl( @@ -121,6 +133,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { backupManager, backupUtils, packageChangeRepository, + userManager, ) } @@ -128,7 +141,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { fun communalWidgets_queryWidgetsFromDb() = testScope.runTest { val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1) - val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L) + val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L, 0) fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry) fakeProviders.value = mapOf(1 to providerInfoA) @@ -154,13 +167,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { fakeWidgets.value = mapOf( CommunalItemRank(uid = 1L, rank = 1) to - CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L), + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0), CommunalItemRank(uid = 2L, rank = 2) to - CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L), + CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0), CommunalItemRank(uid = 3L, rank = 3) to - CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L), + CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L, 0), CommunalItemRank(uid = 4L, rank = 4) to - CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L), + CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L, 0), ) fakeProviders.value = mapOf( @@ -192,9 +205,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { fakeWidgets.value = mapOf( CommunalItemRank(uid = 1L, rank = 1) to - CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L), + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0), CommunalItemRank(uid = 2L, rank = 2) to - CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L), + CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0), ) fakeProviders.value = mapOf( @@ -249,7 +262,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 - val user = UserHandle(0) whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever( @@ -259,11 +271,12 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) ) .thenReturn(id) - underTest.addWidget(provider, user, priority, kosmos.widgetConfiguratorSuccess) + underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorSuccess) runCurrent() - verify(communalWidgetHost).allocateIdAndBindWidget(provider, user) - verify(communalWidgetDao).addWidget(id, provider, priority) + verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser) + verify(communalWidgetDao) + .addWidget(id, provider, priority, testUserSerialNumber(mainUser)) // Verify backup requested verify(backupManager).dataChanged() @@ -275,7 +288,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 - val user = UserHandle(0) whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever( @@ -285,11 +297,12 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) ) .thenReturn(id) - underTest.addWidget(provider, user, priority, kosmos.widgetConfiguratorFail) + underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorFail) runCurrent() - verify(communalWidgetHost).allocateIdAndBindWidget(provider, user) - verify(communalWidgetDao, never()).addWidget(id, provider, priority) + verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser) + verify(communalWidgetDao, never()) + .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt()) verify(appWidgetHost).deleteAppWidgetId(id) // Verify backup not requested @@ -302,7 +315,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 - val user = UserHandle(0) whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever( @@ -312,13 +324,14 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) ) .thenReturn(id) - underTest.addWidget(provider, user, priority) { + underTest.addWidget(provider, mainUser, priority) { throw IllegalStateException("some error") } runCurrent() - verify(communalWidgetHost).allocateIdAndBindWidget(provider, user) - verify(communalWidgetDao, never()).addWidget(id, provider, priority) + verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser) + verify(communalWidgetDao, never()) + .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt()) verify(appWidgetHost).deleteAppWidgetId(id) // Verify backup not requested @@ -331,7 +344,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 - val user = UserHandle(0) whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_CONFIGURATION_OPTIONAL) whenever( @@ -341,11 +353,12 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) ) .thenReturn(id) - underTest.addWidget(provider, user, priority, kosmos.widgetConfiguratorFail) + underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorFail) runCurrent() - verify(communalWidgetHost).allocateIdAndBindWidget(provider, user) - verify(communalWidgetDao).addWidget(id, provider, priority) + verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser) + verify(communalWidgetDao) + .addWidget(id, provider, priority, testUserSerialNumber(mainUser)) // Verify backup requested verify(backupManager).dataChanged() @@ -444,11 +457,8 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { runCurrent() // Verify state restored, and widget 2 skipped - val restoredState = - withArgCaptor<CommunalHubState> { - verify(communalWidgetDao).restoreCommunalHubState(capture()) - } - val restoredWidgets = restoredState.widgets.toList() + verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture()) + val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList() assertThat(restoredWidgets).hasSize(1) val restoredWidget = restoredWidgets.first() @@ -474,11 +484,8 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { runCurrent() // Verify widget 1 and 2 are restored, and are now 11 and 12. - val restoredState = - withArgCaptor<CommunalHubState> { - verify(communalWidgetDao).restoreCommunalHubState(capture()) - } - val restoredWidgets = restoredState.widgets.toList() + verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture()) + val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList() assertThat(restoredWidgets).hasSize(2) val restoredWidget1 = restoredWidgets[0] @@ -512,11 +519,8 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { runCurrent() // Verify widget 1 and 2 are restored, and are now 1 and 12. - val restoredState = - withArgCaptor<CommunalHubState> { - verify(communalWidgetDao).restoreCommunalHubState(capture()) - } - val restoredWidgets = restoredState.widgets.toList() + verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture()) + val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList() assertThat(restoredWidgets).hasSize(2) val restoredWidget1 = restoredWidgets[0] @@ -533,14 +537,134 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test + fun restoreWidgets_undefinedUser_restoredAsMain() = + testScope.runTest { + // Write two widgets to file, both of which have user serial number undefined. + val fakeState = + CommunalHubState().apply { + widgets = + listOf( + CommunalHubState.CommunalWidgetItem().apply { + widgetId = 1 + componentName = "pk_name/fake_widget_1" + rank = 1 + userSerialNumber = + CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED + }, + CommunalHubState.CommunalWidgetItem().apply { + widgetId = 2 + componentName = "pk_name/fake_widget_2" + rank = 2 + userSerialNumber = + CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED + }, + ) + .toTypedArray() + } + backupUtils.writeBytesToDisk(fakeState.toByteArray()) + + // Set up app widget host with widget ids. + setAppWidgetIds(listOf(11, 12)) + + // Restore widgets. + underTest.restoreWidgets(mapOf(Pair(1, 11), Pair(2, 12))) + runCurrent() + + // Verify widget 1 and 2 are restored with the main user. + verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture()) + val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList() + assertThat(restoredWidgets).hasSize(2) + + val restoredWidget1 = restoredWidgets[0] + assertThat(restoredWidget1.widgetId).isEqualTo(11) + assertThat(restoredWidget1.userSerialNumber).isEqualTo(testUserSerialNumber(mainUser)) + + val restoredWidget2 = restoredWidgets[1] + assertThat(restoredWidget2.widgetId).isEqualTo(12) + assertThat(restoredWidget2.userSerialNumber).isEqualTo(testUserSerialNumber(mainUser)) + } + + @Test + fun restoreWidgets_workProfileNotRestored_widgetSkipped() = + testScope.runTest { + // Write fake state to file + backupUtils.writeBytesToDisk(fakeStateWithWorkProfile.toByteArray()) + + // Set up app widget host with widget ids. + // (b/349852237) It's possible that the platform restores widgets even though their user + // is not restored. + setAppWidgetIds(listOf(11, 12)) + + // Restore widgets. + underTest.restoreWidgets(mapOf(Pair(1, 11), Pair(2, 12))) + runCurrent() + + // Verify only widget 1 is restored. Widget 2 is skipped because it belongs to a work + // profile, which is not restored. + verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture()) + val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList() + assertThat(restoredWidgets).hasSize(1) + + val restoredWidget = restoredWidgets[0] + assertThat(restoredWidget.widgetId).isEqualTo(11) + assertThat(restoredWidget.userSerialNumber).isEqualTo(testUserSerialNumber(mainUser)) + } + + @Test + fun restoreWidgets_workProfileRestored_manuallyBindWidget() = + testScope.runTest { + // Write fake state to file + backupUtils.writeBytesToDisk(fakeStateWithWorkProfile.toByteArray()) + + // Set up app widget host with widget ids. + // (b/349852237) It's possible that the platform restores widgets even though their user + // is not restored. + setAppWidgetIds(listOf(11, 12)) + + // Restore work profile. + restoreUser(workProfile) + + val newWidgetId = 13 + whenever(communalWidgetHost.allocateIdAndBindWidget(any(), any())) + .thenReturn(newWidgetId) + + // Restore widgets. + underTest.restoreWidgets(mapOf(Pair(1, 11), Pair(2, 12))) + runCurrent() + + // Verify widget 1 is restored. + verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture()) + val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList() + assertThat(restoredWidgets).hasSize(1) + + val restoredWidget = restoredWidgets[0] + assertThat(restoredWidget.widgetId).isEqualTo(11) + assertThat(restoredWidget.userSerialNumber).isEqualTo(testUserSerialNumber(mainUser)) + + // Verify widget 2 (now 12) is removed from platform + verify(appWidgetHost).deleteAppWidgetId(12) + + // Verify work profile widget is manually bound + verify(communalWidgetDao) + .addWidget( + eq(newWidgetId), + componentNameCaptor.capture(), + eq(2), + eq(testUserSerialNumber(workProfile)) + ) + assertThat(componentNameCaptor.firstValue) + .isEqualTo(ComponentName("pk_name", "fake_widget_2")) + } + + @Test fun pendingWidgets() = testScope.runTest { fakeWidgets.value = mapOf( CommunalItemRank(uid = 1L, rank = 1) to - CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L), + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0), CommunalItemRank(uid = 2L, rank = 2) to - CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L), + CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0), ) // Widget 1 is installed @@ -554,7 +678,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { sessionId = 1, packageName = "pk_2", icon = fakeIcon, - user = UserHandle.CURRENT, + user = mainUser, ) ) ) @@ -572,7 +696,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { priority = 2, packageName = "pk_2", icon = fakeIcon, - user = UserHandle.CURRENT, + user = mainUser, ), ) } @@ -583,7 +707,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { fakeWidgets.value = mapOf( CommunalItemRank(uid = 1L, rank = 1) to - CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L), + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0), ) // Widget 1 is pending install @@ -594,7 +718,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { sessionId = 1, packageName = "pk_1", icon = fakeIcon, - user = UserHandle.CURRENT, + user = mainUser, ) ) ) @@ -607,7 +731,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { priority = 1, packageName = "pk_1", icon = fakeIcon, - user = UserHandle.CURRENT, + user = mainUser, ), ) @@ -633,6 +757,20 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray()) } + // Commonly the user id and user serial number are the same, but for testing purposes use a + // simple algorithm to map a user id to a different user serial number to make sure the correct + // value is used. + private fun testUserSerialNumber(user: UserHandle): Int { + return user.identifier + 100 + } + + private fun restoreUser(user: UserHandle) { + whenever(backupManager.getUserForAncestralSerialNumber(user.identifier.toLong())) + .thenReturn(user) + whenever(userManager.getUserSerialNumber(user.identifier)) + .thenReturn(testUserSerialNumber(user)) + } + private companion object { val PROVIDER_INFO_REQUIRES_CONFIGURATION = AppWidgetProviderInfo().apply { configure = ComponentName("test.pkg", "test.cmp") } @@ -650,11 +788,32 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { widgetId = 1 componentName = "pk_name/fake_widget_1" rank = 1 + userSerialNumber = 0 + }, + CommunalHubState.CommunalWidgetItem().apply { + widgetId = 2 + componentName = "pk_name/fake_widget_2" + rank = 2 + userSerialNumber = 0 + }, + ) + .toTypedArray() + } + val fakeStateWithWorkProfile = + CommunalHubState().apply { + widgets = + listOf( + CommunalHubState.CommunalWidgetItem().apply { + widgetId = 1 + componentName = "pk_name/fake_widget_1" + rank = 1 + userSerialNumber = 0 }, CommunalHubState.CommunalWidgetItem().apply { widgetId = 2 componentName = "pk_name/fake_widget_2" rank = 2 + userSerialNumber = 10 }, ) .toTypedArray() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 7b26db50814e..5cdbe9ce5856 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.communal.domain.interactor import android.app.admin.DevicePolicyManager import android.app.admin.devicePolicyManager -import android.app.smartspace.SmartspaceTarget import android.appwidget.AppWidgetProviderInfo import android.content.Intent import android.content.pm.UserInfo @@ -36,14 +35,17 @@ import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.communal.data.model.CommunalSmartspaceTimer import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository +import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository +import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel @@ -69,8 +71,6 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.fakeUserTracker -import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository -import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository @@ -114,7 +114,7 @@ class CommunalInteractorTest : SysuiTestCase() { private lateinit var communalRepository: FakeCommunalSceneRepository private lateinit var mediaRepository: FakeCommunalMediaRepository private lateinit var widgetRepository: FakeCommunalWidgetRepository - private lateinit var smartspaceRepository: FakeSmartspaceRepository + private lateinit var smartspaceRepository: FakeCommunalSmartspaceRepository private lateinit var userRepository: FakeUserRepository private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository @@ -135,7 +135,7 @@ class CommunalInteractorTest : SysuiTestCase() { communalRepository = kosmos.fakeCommunalSceneRepository mediaRepository = kosmos.fakeCommunalMediaRepository widgetRepository = kosmos.fakeCommunalWidgetRepository - smartspaceRepository = kosmos.fakeSmartspaceRepository + smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository userRepository = kosmos.fakeUserRepository keyguardRepository = kosmos.fakeKeyguardRepository editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter @@ -265,44 +265,6 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun smartspace_onlyShowTimersWithRemoteViews() = - testScope.runTest { - // Keyguard showing, and tutorial completed. - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - - // Not a timer - val target1 = mock(SmartspaceTarget::class.java) - whenever(target1.smartspaceTargetId).thenReturn("target1") - whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_WEATHER) - whenever(target1.remoteViews).thenReturn(mock(RemoteViews::class.java)) - whenever(target1.creationTimeMillis).thenReturn(0L) - - // Does not have RemoteViews - val target2 = mock(SmartspaceTarget::class.java) - whenever(target2.smartspaceTargetId).thenReturn("target2") - whenever(target2.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target2.remoteViews).thenReturn(null) - whenever(target2.creationTimeMillis).thenReturn(0L) - - // Timer and has RemoteViews - val target3 = mock(SmartspaceTarget::class.java) - whenever(target3.smartspaceTargetId).thenReturn("target3") - whenever(target3.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target3.remoteViews).thenReturn(mock(RemoteViews::class.java)) - whenever(target3.creationTimeMillis).thenReturn(0L) - - val targets = listOf(target1, target2, target3) - smartspaceRepository.setCommunalSmartspaceTargets(targets) - - val smartspaceContent by collectLastValue(underTest.getOngoingContent(true)) - assertThat(smartspaceContent?.size).isEqualTo(1) - assertThat(smartspaceContent?.get(0)?.key) - .isEqualTo(CommunalContentModel.KEY.smartspace("target3")) - } - - @Test fun smartspaceDynamicSizing_oneCard_fullSize() = testSmartspaceDynamicSizing( totalTargets = 1, @@ -387,12 +349,12 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setKeyguardOccluded(false) tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - val targets = mutableListOf<SmartspaceTarget>() + val targets = mutableListOf<CommunalSmartspaceTimer>() for (index in 0 until totalTargets) { targets.add(smartspaceTimer(index.toString())) } - smartspaceRepository.setCommunalSmartspaceTargets(targets) + smartspaceRepository.setTimers(targets) val smartspaceContent by collectLastValue(underTest.getOngoingContent(true)) assertThat(smartspaceContent?.size).isEqualTo(totalTargets) @@ -441,18 +403,18 @@ class CommunalInteractorTest : SysuiTestCase() { // Timer1 started val timer1 = smartspaceTimer("timer1", timestamp = 1L) - smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1)) + smartspaceRepository.setTimers(listOf(timer1)) // Umo started mediaRepository.mediaActive(timestamp = 2L) // Timer2 started val timer2 = smartspaceTimer("timer2", timestamp = 3L) - smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1, timer2)) + smartspaceRepository.setTimers(listOf(timer1, timer2)) // Timer3 started val timer3 = smartspaceTimer("timer3", timestamp = 4L) - smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1, timer2, timer3)) + smartspaceRepository.setTimers(listOf(timer1, timer2, timer3)) val ongoingContent by collectLastValue(underTest.getOngoingContent(true)) assertThat(ongoingContent?.size).isEqualTo(4) @@ -1089,13 +1051,12 @@ class CommunalInteractorTest : SysuiTestCase() { assertThat(showCommunalFromOccluded).isTrue() } - private fun smartspaceTimer(id: String, timestamp: Long = 0L): SmartspaceTarget { - val timer = mock(SmartspaceTarget::class.java) - whenever(timer.smartspaceTargetId).thenReturn(id) - whenever(timer.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(timer.remoteViews).thenReturn(mock(RemoteViews::class.java)) - whenever(timer.creationTimeMillis).thenReturn(timestamp) - return timer + private fun smartspaceTimer(id: String, timestamp: Long = 0L): CommunalSmartspaceTimer { + return CommunalSmartspaceTimer( + smartspaceTargetId = id, + createdTimestampMillis = timestamp, + remoteViews = mock(RemoteViews::class.java) + ) } private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 0190ccba3caa..a2f6796c6a38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.communal.view.viewmodel -import android.app.smartspace.SmartspaceTarget import android.appwidget.AppWidgetProviderInfo import android.content.ActivityNotFoundException import android.content.Intent @@ -32,10 +31,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.model.CommunalSmartspaceTimer import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor @@ -57,8 +59,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.settings.fakeUserTracker -import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository -import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any @@ -91,7 +91,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { private lateinit var tutorialRepository: FakeCommunalTutorialRepository private lateinit var widgetRepository: FakeCommunalWidgetRepository - private lateinit var smartspaceRepository: FakeSmartspaceRepository + private lateinit var smartspaceRepository: FakeCommunalSmartspaceRepository private lateinit var mediaRepository: FakeCommunalMediaRepository private lateinit var communalSceneInteractor: CommunalSceneInteractor @@ -105,7 +105,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { tutorialRepository = kosmos.fakeCommunalTutorialRepository widgetRepository = kosmos.fakeCommunalWidgetRepository - smartspaceRepository = kosmos.fakeSmartspaceRepository + smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository mediaRepository = kosmos.fakeCommunalMediaRepository communalSceneInteractor = kosmos.communalSceneInteractor kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) @@ -152,11 +152,15 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { widgetRepository.setCommunalWidgets(widgets) // Smartspace available. - val target = Mockito.mock(SmartspaceTarget::class.java) - whenever(target.smartspaceTargetId).thenReturn("target") - whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java)) - smartspaceRepository.setCommunalSmartspaceTargets(listOf(target)) + smartspaceRepository.setTimers( + listOf( + CommunalSmartspaceTimer( + smartspaceTargetId = "target", + createdTimestampMillis = 0L, + remoteViews = Mockito.mock(RemoteViews::class.java), + ) + ) + ) // Media playing. mediaRepository.mediaActive() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index d33877462ec6..74a048d038bd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.communal.view.viewmodel -import android.app.smartspace.SmartspaceTarget import android.appwidget.AppWidgetProviderInfo import android.content.pm.UserInfo import android.os.UserHandle @@ -27,12 +26,15 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.model.CommunalSmartspaceTimer import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository +import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository +import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.communalInteractor @@ -78,8 +80,6 @@ import com.android.systemui.settings.fakeUserTracker import com.android.systemui.shade.ShadeTestUtil import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.shadeTestUtil -import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository -import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository @@ -115,7 +115,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var tutorialRepository: FakeCommunalTutorialRepository private lateinit var widgetRepository: FakeCommunalWidgetRepository - private lateinit var smartspaceRepository: FakeSmartspaceRepository + private lateinit var smartspaceRepository: FakeCommunalSmartspaceRepository private lateinit var mediaRepository: FakeCommunalMediaRepository private lateinit var userRepository: FakeUserRepository private lateinit var shadeTestUtil: ShadeTestUtil @@ -136,7 +136,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository tutorialRepository = kosmos.fakeCommunalTutorialRepository widgetRepository = kosmos.fakeCommunalWidgetRepository - smartspaceRepository = kosmos.fakeSmartspaceRepository + smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository mediaRepository = kosmos.fakeCommunalMediaRepository userRepository = kosmos.fakeUserRepository shadeTestUtil = kosmos.shadeTestUtil @@ -222,11 +222,15 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { widgetRepository.setCommunalWidgets(widgets) // Smartspace available. - val target = Mockito.mock(SmartspaceTarget::class.java) - whenever(target.smartspaceTargetId).thenReturn("target") - whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java)) - smartspaceRepository.setCommunalSmartspaceTargets(listOf(target)) + smartspaceRepository.setTimers( + listOf( + CommunalSmartspaceTimer( + smartspaceTargetId = "target", + createdTimestampMillis = 0L, + remoteViews = Mockito.mock(RemoteViews::class.java), + ) + ) + ) // Media playing. mediaRepository.mediaActive() @@ -293,7 +297,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { widgetRepository.setCommunalWidgets(emptyList()) // UMO playing mediaRepository.mediaActive() - smartspaceRepository.setCommunalSmartspaceTargets(emptyList()) + smartspaceRepository.setTimers(emptyList()) val isEmptyState by collectLastValue(underTest.isEmptyState) assertThat(isEmptyState).isTrue() @@ -314,7 +318,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { ), ) mediaRepository.mediaInactive() - smartspaceRepository.setCommunalSmartspaceTargets(emptyList()) + smartspaceRepository.setTimers(emptyList()) val isEmptyState by collectLastValue(underTest.isEmptyState) assertThat(isEmptyState).isFalse() @@ -689,11 +693,15 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { advanceTimeBy(60L) // New timer available - val target = Mockito.mock(SmartspaceTarget::class.java) - whenever<String?>(target.smartspaceTargetId).thenReturn("target") - whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java)) - smartspaceRepository.setCommunalSmartspaceTargets(listOf(target)) + smartspaceRepository.setTimers( + listOf( + CommunalSmartspaceTimer( + smartspaceTargetId = "target", + createdTimestampMillis = 0L, + remoteViews = Mockito.mock(RemoteViews::class.java), + ) + ) + ) runCurrent() // Still only emits widgets and the CTA tile @@ -748,11 +756,15 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(communalContent).hasSize(3) // When new timer available - val target = Mockito.mock(SmartspaceTarget::class.java) - whenever(target.smartspaceTargetId).thenReturn("target") - whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java)) - smartspaceRepository.setCommunalSmartspaceTargets(listOf(target)) + smartspaceRepository.setTimers( + listOf( + CommunalSmartspaceTimer( + smartspaceTargetId = "target", + createdTimestampMillis = 0L, + remoteViews = Mockito.mock(RemoteViews::class.java), + ) + ) + ) runCurrent() // Then emits timer, widgets and the CTA tile diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index 5a39de8392be..444f63afb021 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -34,6 +34,9 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.app.viewcapture.ViewCapture +import com.android.app.viewcapture.ViewCaptureAwareWindowManager +import com.android.app.viewcapture.ViewCaptureFactory import com.android.compose.animation.scene.ObservableTransitionState import com.android.internal.logging.UiEventLogger import com.android.keyguard.KeyguardUpdateMonitor @@ -79,6 +82,7 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.isNull +import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -114,11 +118,11 @@ class DreamOverlayServiceTest : SysuiTestCase() { @Mock lateinit var mDreamComplicationComponentFactory: - com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory + com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory @Mock lateinit var mDreamComplicationComponent: - com.android.systemui.dreams.complication.dagger.ComplicationComponent + com.android.systemui.dreams.complication.dagger.ComplicationComponent @Mock lateinit var mHideComplicationTouchHandler: HideComplicationTouchHandler @@ -154,8 +158,12 @@ class DreamOverlayServiceTest : SysuiTestCase() { @Mock lateinit var mDreamOverlayCallbackController: DreamOverlayCallbackController + @Mock lateinit var mLazyViewCapture: Lazy<ViewCapture> + + private lateinit var mViewCaptureAwareWindowManager: ViewCaptureAwareWindowManager private lateinit var bouncerRepository: FakeKeyguardBouncerRepository private lateinit var communalRepository: FakeCommunalSceneRepository + private var viewCaptureSpy = spy(ViewCaptureFactory.getInstance(context)) @Captor var mViewCaptor: ArgumentCaptor<View>? = null private lateinit var mService: DreamOverlayService @@ -192,13 +200,16 @@ class DreamOverlayServiceTest : SysuiTestCase() { whenever(mDreamOverlayContainerViewController.containerView) .thenReturn(mDreamOverlayContainerView) whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController) + whenever(mLazyViewCapture.value).thenReturn(viewCaptureSpy) mWindowParams = WindowManager.LayoutParams() + mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager(mWindowManager, + mLazyViewCapture, isViewCaptureEnabled = false) mService = DreamOverlayService( mContext, mLifecycleOwner, mMainExecutor, - mWindowManager, + mViewCaptureAwareWindowManager, mComplicationComponentFactory, mDreamComplicationComponentFactory, mDreamOverlayComponentFactory, @@ -246,7 +257,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START) verify(mUiEventLogger) - .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START) + .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt index 74eee9b24cbc..693fcdabea58 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt @@ -17,10 +17,12 @@ package com.android.systemui.haptics.qs import android.os.VibrationEffect +import android.service.quicksettings.Tile import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.falsingManager import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.testScope import com.android.systemui.qs.qsTileFactory @@ -69,6 +71,7 @@ class QSLongPressEffectTest : SysuiTestCase() { QSLongPressEffect( vibratorHelper, kosmos.keyguardStateController, + kosmos.falsingManager, ) longPressEffect.callback = callback longPressEffect.qsTile = qsTile @@ -175,17 +178,17 @@ class QSLongPressEffectTest : SysuiTestCase() { } @Test - fun onAnimationComplete_keyguardDismissible_effectCompletes() = + fun onAnimationComplete_keyguardDismissible_effectEndsInLongClicked() = testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() - // THEN the long-press effect completes - assertEffectCompleted() + // THEN the long-press effect completes with a long-click state + assertEffectCompleted(QSLongPressEffect.State.LONG_CLICKED) } @Test - fun onAnimationComplete_keyguardNotDismissible_effectEndsWithReset() = + fun onAnimationComplete_keyguardNotDismissible_effectEndsInIdleWithReset() = testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { // GIVEN that the keyguard is not dismissible whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) @@ -193,19 +196,20 @@ class QSLongPressEffectTest : SysuiTestCase() { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() - // THEN the long-press effect completes and the properties are called to reset - assertEffectCompleted() + // THEN the long-press effect ends in the idle state and the properties are reset + assertEffectCompleted(QSLongPressEffect.State.IDLE) verify(callback, times(1)).onResetProperties() } @Test - fun onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversing() = + fun onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversingAndClick() = testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP) { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() - // THEN the callback for finished reversing is used. + // THEN the callback for finished reversing is used and the effect ends with a click. verify(callback, times(1)).onEffectFinishedReversing() + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.CLICKED) } @Test @@ -262,18 +266,88 @@ class QSLongPressEffectTest : SysuiTestCase() { } @Test - fun onTileClick_whileWaiting_withoutQSTile_cannotClick() = - testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) { - // GIVEN that no QSTile has been set - longPressEffect.qsTile = null - + fun onTileClick_whileIdle_withQSTile_clicks() = + testWhileInState(QSLongPressEffect.State.IDLE) { // GIVEN that a click was detected val couldClick = longPressEffect.onTileClick() + // THEN the click is successful + assertThat(couldClick).isTrue() + } + + @Test + fun onTileClick_whenBouncerIsShowing_ignoresClick() = + testWhileInState(QSLongPressEffect.State.IDLE) { + // GIVEN that the bouncer is showing + whenever(kosmos.keyguardStateController.isPrimaryBouncerShowing).thenReturn(true) + + // WHEN a click is detected by the tile view + val couldClick = longPressEffect.onTileClick() + // THEN the click is not successful assertThat(couldClick).isFalse() } + @Test + fun getStateForClick_withUnavailableTile_returnsIdle() { + // GIVEN an unavailable tile + qsTile.state?.state = Tile.STATE_UNAVAILABLE + + // WHEN determining the state of a click action + val clickState = longPressEffect.getStateForClick() + + // THEN the state is IDLE + assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) + } + + @Test + fun getStateForClick_withFalseTapWhenLocked_returnsIdle() { + // GIVEN an active tile + qsTile.state?.state = Tile.STATE_ACTIVE + + // GIVEN that the device is locked and a false tap is detected + whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) + kosmos.falsingManager.setFalseTap(true) + + // WHEN determining the state of a click action + val clickState = longPressEffect.getStateForClick() + + // THEN the state is IDLE + assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) + } + + @Test + fun getStateForClick_withValidTapAndTile_returnsClicked() { + // GIVEN an active tile + qsTile.state?.state = Tile.STATE_ACTIVE + + // GIVEN that the device is locked and a false tap is not detected + whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) + kosmos.falsingManager.setFalseTap(false) + + // WHEN determining the state of a click action + val clickState = longPressEffect.getStateForClick() + + // THEN the state is CLICKED + assertThat(clickState).isEqualTo(QSLongPressEffect.State.CLICKED) + } + + @Test + fun getStateForClick_withNullTile_returnsIdle() { + // GIVEN that the tile is null + longPressEffect.qsTile = null + + // GIVEN that the device is locked and a false tap is not detected + whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false) + kosmos.falsingManager.setFalseTap(false) + + // WHEN determining the state of a click action + val clickState = longPressEffect.getStateForClick() + + // THEN the state is IDLE + assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) + } + private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) = with(kosmos) { testScope.runTest { @@ -339,14 +413,14 @@ class QSLongPressEffectTest : SysuiTestCase() { /** * Asserts that the effect completes by checking that: * 1. The final snap haptics are played - * 2. The internal state goes back to [QSLongPressEffect.State.IDLE] + * 2. The internal state goes back to specified end state. */ - private fun assertEffectCompleted() { + private fun assertEffectCompleted(endState: QSLongPressEffect.State) { val snapEffect = LongPressHapticBuilder.createSnapEffect() assertThat(snapEffect).isNotNull() assertThat(vibratorHelper.hasVibratedWithEffects(snapEffect!!)).isTrue() - assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) + assertThat(longPressEffect.state).isEqualTo(endState) } /** diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index 612f2e73e4bb..ec4fd79b399a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -34,12 +34,14 @@ package com.android.systemui.keyguard.domain.interactor import android.os.PowerManager import android.platform.test.annotations.EnableFlags +import android.service.dream.dreamManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository +import com.android.systemui.communal.domain.interactor.setCommunalAvailable import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -64,8 +66,10 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.reset import org.mockito.Mockito.spy +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -120,6 +124,66 @@ class FromDozingTransitionInteractorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToLockscreen_onPowerButtonPress_canDream_glanceableHubAvailable() = + testScope.runTest { + whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + kosmos.setCommunalAvailable(true) + runCurrent() + + powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON) + runCurrent() + + // If dreaming is possible and communal is available, then we should transition to + // GLANCEABLE_HUB when waking up due to power button press. + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.DOZING, + to = KeyguardState.GLANCEABLE_HUB, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToLockscreen_onPowerButtonPress_canNotDream_glanceableHubAvailable() = + testScope.runTest { + whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(false) + kosmos.setCommunalAvailable(true) + runCurrent() + + powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON) + runCurrent() + + // If dreaming is NOT possible but communal is available, then we should transition to + // LOCKSCREEN when waking up due to power button press. + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.DOZING, + to = KeyguardState.LOCKSCREEN, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToLockscreen_onPowerButtonPress_canNDream_glanceableHubNotAvailable() = + testScope.runTest { + whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + kosmos.setCommunalAvailable(false) + runCurrent() + + powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON) + runCurrent() + + // If dreaming is possible but communal is NOT available, then we should transition to + // LOCKSCREEN when waking up due to power button press. + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.DOZING, + to = KeyguardState.LOCKSCREEN, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionToGlanceableHub_onWakeup_ifIdleOnCommunal_noOccludingActivity() = testScope.runTest { kosmos.fakeCommunalSceneRepository.setTransitionState( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt index 26b56a1be926..48621047016b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt @@ -37,7 +37,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository @@ -81,10 +80,10 @@ class KeyguardClockInteractorTest : SysuiTestCase() { testScope.runTest { val value by collectLastValue(underTest.clockShouldBeCentered) kosmos.keyguardInteractor.setClockShouldBeCentered(true) - assertThat(value).isEqualTo(true) + assertThat(value).isTrue() kosmos.keyguardInteractor.setClockShouldBeCentered(false) - assertThat(value).isEqualTo(false) + assertThat(value).isFalse() } @Test @@ -103,7 +102,7 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockSize_SceneContainerFlagOn_shadeModeSingle_hasNotifs_SMALL() = testScope.runTest { val value by collectLastValue(underTest.clockSize) - kosmos.shadeRepository.setShadeMode(ShadeMode.Single) + kosmos.shadeRepository.setShadeLayoutWide(false) kosmos.activeNotificationListRepository.setActiveNotifs(1) assertThat(value).isEqualTo(ClockSize.SMALL) } @@ -113,7 +112,7 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockSize_SceneContainerFlagOn_shadeModeSingle_hasMedia_SMALL() = testScope.runTest { val value by collectLastValue(underTest.clockSize) - kosmos.shadeRepository.setShadeMode(ShadeMode.Single) + kosmos.shadeRepository.setShadeLayoutWide(false) val userMedia = MediaData().copy(active = true) kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) assertThat(value).isEqualTo(ClockSize.SMALL) @@ -125,7 +124,7 @@ class KeyguardClockInteractorTest : SysuiTestCase() { testScope.runTest { val value by collectLastValue(underTest.clockSize) val userMedia = MediaData().copy(active = true) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) kosmos.keyguardRepository.setIsDozing(false) assertThat(value).isEqualTo(ClockSize.SMALL) @@ -136,7 +135,7 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockSize_SceneContainerFlagOn_shadeModeSplit_noMedia_LARGE() = testScope.runTest { val value by collectLastValue(underTest.clockSize) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.keyguardRepository.setIsDozing(false) assertThat(value).isEqualTo(ClockSize.LARGE) } @@ -147,7 +146,7 @@ class KeyguardClockInteractorTest : SysuiTestCase() { testScope.runTest { val value by collectLastValue(underTest.clockSize) val userMedia = MediaData().copy(active = true) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) kosmos.keyguardRepository.setIsDozing(true) assertThat(value).isEqualTo(ClockSize.LARGE) @@ -158,8 +157,8 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockShouldBeCentered_sceneContainerFlagOn_notSplitMode_true() = testScope.runTest { val value by collectLastValue(underTest.clockShouldBeCentered) - kosmos.shadeRepository.setShadeMode(ShadeMode.Single) - assertThat(value).isEqualTo(true) + kosmos.shadeRepository.setShadeLayoutWide(false) + assertThat(value).isTrue() } @Test @@ -167,9 +166,9 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockShouldBeCentered_sceneContainerFlagOn_splitMode_noActiveNotifications_true() = testScope.runTest { val value by collectLastValue(underTest.clockShouldBeCentered) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.activeNotificationListRepository.setActiveNotifs(0) - assertThat(value).isEqualTo(true) + assertThat(value).isTrue() } @Test @@ -177,10 +176,10 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockShouldBeCentered_sceneContainerFlagOn_splitMode_isActiveDreamLockscreenHosted_true() = testScope.runTest { val value by collectLastValue(underTest.clockShouldBeCentered) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.activeNotificationListRepository.setActiveNotifs(1) kosmos.keyguardRepository.setIsActiveDreamLockscreenHosted(true) - assertThat(value).isEqualTo(true) + assertThat(value).isTrue() } @Test @@ -188,11 +187,11 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockShouldBeCentered_sceneContainerFlagOn_splitMode_hasPulsingNotifications_false() = testScope.runTest { val value by collectLastValue(underTest.clockShouldBeCentered) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.activeNotificationListRepository.setActiveNotifs(1) kosmos.headsUpNotificationRepository.isHeadsUpAnimatingAway.value = true kosmos.keyguardRepository.setIsDozing(true) - assertThat(value).isEqualTo(false) + assertThat(value).isFalse() } @Test @@ -200,10 +199,10 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockShouldBeCentered_sceneContainerFlagOn_splitMode_onAod_true() = testScope.runTest { val value by collectLastValue(underTest.clockShouldBeCentered) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.activeNotificationListRepository.setActiveNotifs(1) transitionTo(KeyguardState.LOCKSCREEN, KeyguardState.AOD) - assertThat(value).isEqualTo(true) + assertThat(value).isTrue() } @Test @@ -211,10 +210,10 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockShouldBeCentered_sceneContainerFlagOn_splitMode_offAod_false() = testScope.runTest { val value by collectLastValue(underTest.clockShouldBeCentered) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.activeNotificationListRepository.setActiveNotifs(1) transitionTo(KeyguardState.AOD, KeyguardState.LOCKSCREEN) - assertThat(value).isEqualTo(false) + assertThat(value).isFalse() } private suspend fun transitionTo(from: KeyguardState, to: KeyguardState) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt index 875e9e0210fb..50772eedc914 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt @@ -37,7 +37,6 @@ import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider import com.android.systemui.util.mockito.whenever @@ -76,7 +75,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa fun setup() { with(kosmos) { fakeFeatureFlagsClassic.set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, true) - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) underTest = lockscreenContentViewModel } } @@ -126,7 +125,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa with(kosmos) { testScope.runTest { val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible) - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE) assertThat(areNotificationsVisible).isTrue() @@ -156,24 +155,24 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test - fun shouldUseSplitNotificationShade_withConfigTrue_true() = + fun isShadeLayoutWide_withConfigTrue_true() = with(kosmos) { testScope.runTest { - val shouldUseSplitNotificationShade by - collectLastValue(underTest.shouldUseSplitNotificationShade) - shadeRepository.setShadeMode(ShadeMode.Split) - assertThat(shouldUseSplitNotificationShade).isTrue() + val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) + shadeRepository.setShadeLayoutWide(true) + + assertThat(isShadeLayoutWide).isTrue() } } @Test - fun shouldUseSplitNotificationShade_withConfigFalse_false() = + fun isShadeLayoutWide_withConfigFalse_false() = with(kosmos) { testScope.runTest { - val shouldUseSplitNotificationShade by - collectLastValue(underTest.shouldUseSplitNotificationShade) - shadeRepository.setShadeMode(ShadeMode.Single) - assertThat(shouldUseSplitNotificationShade).isFalse() + val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) + shadeRepository.setShadeLayoutWide(false) + + assertThat(isShadeLayoutWide).isFalse() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 4eb146dbbaba..3db9ef1eca71 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -44,7 +44,6 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock @@ -182,13 +181,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { } ) sceneInteractor.changeScene(Scenes.Lockscreen, "reason") - kosmos.shadeRepository.setShadeMode( - if (isSingleShade) { - ShadeMode.Single - } else { - ShadeMode.Split - } - ) + kosmos.shadeRepository.setShadeLayoutWide(!isSingleShade) kosmos.setCommunalAvailable(isCommunalAvailable) kosmos.fakePowerRepository.updateWakefulness( rawState = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/SettingObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/SettingObserverTest.kt new file mode 100644 index 000000000000..188f2aca5147 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/SettingObserverTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.qs + +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.settings.SettingsProxy +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.capture +import org.mockito.kotlin.eq +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SettingObserverTest : SysuiTestCase() { + + private val DEFAULT_VALUE = 7 + + @Mock lateinit var settingsProxy: SettingsProxy + @Captor private lateinit var argumentCaptor: ArgumentCaptor<Runnable> + + private lateinit var testSettingObserver: SettingObserver + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(settingsProxy.getInt(any(), any())).thenReturn(5) + whenever(settingsProxy.getUriFor(any())).thenReturn(Uri.parse("content://test_uri")) + testSettingObserver = + object : + SettingObserver( + settingsProxy, + Handler(Looper.getMainLooper()), + "test_setting", + DEFAULT_VALUE + ) { + override fun handleValueChanged(value: Int, observedChange: Boolean) {} + } + } + + @Test + @EnableFlags(Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD) + fun setListening_true_settingsProxyRegistered() { + testSettingObserver.isListening = true + verify(settingsProxy) + .registerContentObserverAsync( + any<Uri>(), + eq(false), + eq(testSettingObserver), + capture(argumentCaptor) + ) + assertThat(testSettingObserver.value).isEqualTo(5) + + // Verify if the callback applies updated value after the fact + whenever(settingsProxy.getInt(any(), any())).thenReturn(12341234) + argumentCaptor.value.run() + assertThat(testSettingObserver.value).isEqualTo(12341234) + } + + @Test + @EnableFlags(Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD) + fun setListening_false_settingsProxyRegistered() { + testSettingObserver.isListening = true + reset(settingsProxy) + testSettingObserver.isListening = false + + verify(settingsProxy).unregisterContentObserverAsync(eq(testSettingObserver)) + } + + @Test + @DisableFlags(Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD) + fun setListening_bgFlagDisabled_true_settingsProxyRegistered() { + testSettingObserver.isListening = true + verify(settingsProxy) + .registerContentObserverSync(any<Uri>(), eq(false), eq(testSettingObserver)) + } + + @Test + @DisableFlags(Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD) + fun setListening_bgFlagDisabled_false_settingsProxyRegistered() { + testSettingObserver.isListening = true + reset(settingsProxy) + testSettingObserver.isListening = false + + verify(settingsProxy).unregisterContentObserverSync(eq(testSettingObserver)) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileDataInteractorTest.kt index 89b9b7f30297..67e2fba30822 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileDataInteractorTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.impl.airplate.domain.interactor +package com.android.systemui.qs.tiles.impl.airplane.domain.interactor import android.os.UserHandle import android.platform.test.annotations.EnabledOnRavenwood @@ -23,7 +23,6 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger -import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt index 8982d810ad8a..79fcc92a967c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.impl.airplate.domain.interactor +package com.android.systemui.qs.tiles.impl.airplane.domain.interactor import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings @@ -26,7 +26,6 @@ import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandl import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject.Companion.assertThat import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.click import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.longClick -import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor @@ -54,7 +53,7 @@ class AirplaneModeTileUserActionInteractorTest : SysuiTestCase() { connectivityRepository, mobileConnectionsRepository, ), - inputHandler + inputHandler, ) @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt new file mode 100644 index 000000000000..583c10fe429e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.modes.domain.interactor + +import android.app.Flags +import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS +import android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS +import android.provider.Settings.Global.ZEN_MODE_OFF +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.notification.data.repository.FakeZenModeRepository +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toCollection +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class ModesTileDataInteractorTest : SysuiTestCase() { + private val zenModeRepository = FakeZenModeRepository() + + private val underTest = ModesTileDataInteractor(zenModeRepository) + + @EnableFlags(Flags.FLAG_MODES_UI) + @Test + fun availableWhenFlagIsOn() = runTest { + val availability = underTest.availability(TEST_USER).toCollection(mutableListOf()) + + assertThat(availability).containsExactly(true) + } + + @DisableFlags(Flags.FLAG_MODES_UI) + @Test + fun unavailableWhenFlagIsOff() = runTest { + val availability = underTest.availability(TEST_USER).toCollection(mutableListOf()) + + assertThat(availability).containsExactly(false) + } + + @EnableFlags(Flags.FLAG_MODES_UI) + @Test + fun dataMatchesTheRepository() = runTest { + val dataList: List<ModesTileModel> by + collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))) + runCurrent() + + // Enable zen mode + zenModeRepository.updateZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS) + runCurrent() + + // Change zen mode: it's still enabled, so this shouldn't cause another emission + zenModeRepository.updateZenMode(ZEN_MODE_NO_INTERRUPTIONS) + runCurrent() + + // Disable zen mode + zenModeRepository.updateZenMode(ZEN_MODE_OFF) + runCurrent() + + assertThat(dataList.map { it.isActivated }).containsExactly(false, true, false) + } + + private companion object { + + val TEST_USER = UserHandle.of(1)!! + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractorTest.kt index 2e5fde8e4bd6..a5f98a739b49 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractorTest.kt @@ -30,7 +30,6 @@ import com.android.systemui.qs.tiles.impl.reducebrightness.domain.model.ReduceBr import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -56,8 +55,7 @@ class ReduceBrightColorsTileDataInteractorTest : SysuiTestCase() { @Test fun alwaysAvailable() = testScope.runTest { - val availability = underTest.availability(TEST_USER).toCollection(mutableListOf()) - + val availability by collectValues(underTest.availability(TEST_USER)) assertThat(availability).hasSize(1) assertThat(availability.last()).isEqualTo(isAvailable) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt index 6ea5e63fdff6..313331286b45 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt @@ -17,9 +17,13 @@ package com.android.systemui.qs.tiles.impl.reducebrightness.domain.interactor import android.platform.test.annotations.EnabledOnRavenwood +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.annotations.RequiresFlagsEnabled import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.server.display.feature.flags.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.accessibility.reduceBrightColorsController import com.android.systemui.kosmos.Kosmos @@ -43,11 +47,22 @@ class ReduceBrightColorsTileUserActionInteractorTest : SysuiTestCase() { private val underTest = ReduceBrightColorsTileUserActionInteractor( + context.resources, + inputHandler, + controller, + ) + + private val underTestEvenDimmerEnabled = + ReduceBrightColorsTileUserActionInteractor( + context.orCreateTestableResources + .apply { addOverride(R.bool.config_evenDimmerEnabled, true) } + .resources, inputHandler, controller, ) @Test + @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER) fun handleClickWhenEnabled() = runTest { val wasEnabled = true controller.isReduceBrightColorsActivated = wasEnabled @@ -58,6 +73,7 @@ class ReduceBrightColorsTileUserActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER) fun handleClickWhenDisabled() = runTest { val wasEnabled = false controller.isReduceBrightColorsActivated = wasEnabled @@ -68,6 +84,7 @@ class ReduceBrightColorsTileUserActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER) fun handleLongClickWhenDisabled() = runTest { val enabled = false @@ -79,6 +96,7 @@ class ReduceBrightColorsTileUserActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER) fun handleLongClickWhenEnabled() = runTest { val enabled = true @@ -88,4 +106,58 @@ class ReduceBrightColorsTileUserActionInteractorTest : SysuiTestCase() { assertThat(it.intent.action).isEqualTo(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS) } } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + fun handleClickWhenEnabledEvenDimmer() = runTest { + val wasEnabled = true + controller.isReduceBrightColorsActivated = wasEnabled + + underTestEvenDimmerEnabled.handleInput( + QSTileInputTestKtx.click(ReduceBrightColorsTileModel(wasEnabled)) + ) + + assertThat(controller.isReduceBrightColorsActivated).isEqualTo(wasEnabled) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + fun handleClickWhenDisabledEvenDimmer() = runTest { + val wasEnabled = false + controller.isReduceBrightColorsActivated = wasEnabled + + underTestEvenDimmerEnabled.handleInput( + QSTileInputTestKtx.click(ReduceBrightColorsTileModel(wasEnabled)) + ) + + assertThat(controller.isReduceBrightColorsActivated).isEqualTo(wasEnabled) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + fun handleLongClickWhenDisabledEvenDimmer() = runTest { + val enabled = false + + underTestEvenDimmerEnabled.handleInput( + QSTileInputTestKtx.longClick(ReduceBrightColorsTileModel(enabled)) + ) + + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { + assertThat(it.intent.action).isEqualTo(Settings.ACTION_DISPLAY_SETTINGS) + } + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + fun handleLongClickWhenEnabledEvenDimmer() = runTest { + val enabled = true + + underTestEvenDimmerEnabled.handleInput( + QSTileInputTestKtx.longClick(ReduceBrightColorsTileModel(enabled)) + ) + + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { + assertThat(it.intent.action).isEqualTo(Settings.ACTION_DISPLAY_SETTINGS) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt index b35b7bca4809..09580c5f17be 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt @@ -38,7 +38,6 @@ import com.android.systemui.qs.dagger.QSSceneComponent import com.android.systemui.settings.brightness.MirrorController import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture @@ -558,7 +557,7 @@ class QSSceneAdapterImplTest : SysuiTestCase() { fun dispatchSplitShade() = testScope.runTest { val shadeRepository = kosmos.fakeShadeRepository - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) val qsImpl by collectLastValue(underTest.qsImpl) underTest.inflate(context) @@ -566,7 +565,7 @@ class QSSceneAdapterImplTest : SysuiTestCase() { verify(qsImpl!!).setInSplitShade(false) - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) runCurrent() verify(qsImpl!!).setInSplitShade(true) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt index 545a0c70719f..0ab6a8250dcf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt @@ -28,7 +28,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -63,7 +62,7 @@ class GoneSceneViewModelTest : SysuiTestCase() { fun downTransitionKey_splitShadeEnabled_isGoneToSplitShade() = testScope.runTest { val destinationScenes by collectLastValue(underTest.destinationScenes) - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) runCurrent() assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey) @@ -74,7 +73,7 @@ class GoneSceneViewModelTest : SysuiTestCase() { fun downTransitionKey_splitShadeDisabled_isNull() = testScope.runTest { val destinationScenes by collectLastValue(underTest.destinationScenes) - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) runCurrent() assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey).isNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt index 78c4def5689b..3283ea154b3f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt @@ -19,6 +19,8 @@ package com.android.systemui.shade.domain.interactor import android.app.StatusBarManager.DISABLE2_NONE import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -37,7 +39,10 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.shadeTestUtil +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.phone.dozeParameters @@ -452,4 +457,44 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) assertThat(isShadeTouchable).isTrue() } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun legacyShadeMode_narrowScreen_singleShade() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(false) + + assertThat(shadeMode).isEqualTo(ShadeMode.Single) + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun legacyShadeMode_wideScreen_splitShade() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(true) + + assertThat(shadeMode).isEqualTo(ShadeMode.Split) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun shadeMode_wideScreen_isDual() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(true) + + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun shadeMode_narrowScreen_isDual() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(false) + + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt index b1bffdbb1bed..8a4319805802 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt @@ -29,7 +29,6 @@ import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat @@ -568,21 +567,6 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { sceneInteractor.setTransitionState(transitionState) // THEN interacting is false - Truth.assertThat(interacting).isFalse() - } - - @Test - fun shadeMode() = - testScope.runTest { - val shadeMode by collectLastValue(underTest.shadeMode) - - shadeRepository.setShadeMode(ShadeMode.Split) - assertThat(shadeMode).isEqualTo(ShadeMode.Split) - - shadeRepository.setShadeMode(ShadeMode.Single) - assertThat(shadeMode).isEqualTo(ShadeMode.Single) - - shadeRepository.setShadeMode(ShadeMode.Split) - assertThat(shadeMode).isEqualTo(ShadeMode.Split) + assertThat(interacting).isFalse() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 673d5ef5d962..da22c6d7419d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade.ui.viewmodel +import android.platform.test.annotations.DisableFlags import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -38,7 +39,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.qs.ui.adapter.fakeQSSceneAdapter -import com.android.systemui.qs.ui.adapter.qsSceneAdapter import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver @@ -47,6 +47,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.startable.shadeStartable +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider @@ -66,6 +67,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @EnableSceneContainer +@DisableFlags(DualShade.FLAG_NAME) class ShadeSceneViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -157,7 +159,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionKey_splitShadeEnabled_isGoneToSplitShade() = testScope.runTest { val destinationScenes by collectLastValue(underTest.destinationScenes) - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) runCurrent() assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey) @@ -168,7 +170,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionKey_splitShadeDisable_isNull() = testScope.runTest { val destinationScenes by collectLastValue(underTest.destinationScenes) - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) runCurrent() assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey).isNull() @@ -268,13 +270,13 @@ class ShadeSceneViewModelTest : SysuiTestCase() { testScope.runTest { val shadeMode by collectLastValue(underTest.shadeMode) - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) assertThat(shadeMode).isEqualTo(ShadeMode.Split) - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) assertThat(shadeMode).isEqualTo(ShadeMode.Single) - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) assertThat(shadeMode).isEqualTo(ShadeMode.Split) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt index 50b77dcf9468..1356e93db549 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt @@ -22,7 +22,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding import com.android.systemui.testKosmos @@ -66,11 +65,11 @@ class NotificationStackAppearanceInteractorTest : SysuiTestCase() { testScope.runTest { val stackRounding by collectLastValue(underTest.shadeScrimRounding) - kosmos.shadeRepository.setShadeMode(ShadeMode.Single) + kosmos.shadeRepository.setShadeLayoutWide(false) assertThat(stackRounding) .isEqualTo(ShadeScrimRounding(isTopRounded = true, isBottomRounded = false)) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) assertThat(stackRounding) .isEqualTo(ShadeScrimRounding(isTopRounded = true, isBottomRounded = true)) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java deleted file mode 100644 index 3d3438eab109..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.policy; - -import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; -import static com.android.systemui.util.concurrency.MockExecutorHandlerKt.mockExecutorHandler; - -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.os.Handler; -import android.platform.test.flag.junit.FlagsParameterization; -import android.testing.TestableLooper; - -import androidx.test.filters.SmallTest; - -import com.android.internal.logging.UiEventLogger; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.res.R; -import com.android.systemui.shade.domain.interactor.ShadeInteractor; -import com.android.systemui.statusbar.NotificationShadeWindowController; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; -import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; -import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; -import com.android.systemui.statusbar.phone.ConfigurationControllerImpl; -import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; -import com.android.systemui.statusbar.phone.KeyguardBypassController; -import com.android.systemui.util.concurrency.DelayableExecutor; -import com.android.systemui.util.kotlin.JavaAdapter; -import com.android.systemui.util.settings.GlobalSettings; -import com.android.systemui.util.time.SystemClock; - -import kotlinx.coroutines.flow.StateFlowKt; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - -import java.util.List; - -@SmallTest -@RunWith(ParameterizedAndroidJunit4.class) -@TestableLooper.RunWithLooper -public class HeadsUpManagerPhoneTest extends BaseHeadsUpManagerTest { - @Rule public MockitoRule rule = MockitoJUnit.rule(); - - private final HeadsUpManagerLogger mHeadsUpManagerLogger = new HeadsUpManagerLogger( - logcatLogBuffer()); - @Mock private GroupMembershipManager mGroupManager; - @Mock private VisualStabilityProvider mVSProvider; - @Mock private StatusBarStateController mStatusBarStateController; - @Mock private KeyguardBypassController mBypassController; - @Mock private ConfigurationControllerImpl mConfigurationController; - @Mock private AccessibilityManagerWrapper mAccessibilityManagerWrapper; - @Mock private UiEventLogger mUiEventLogger; - @Mock private JavaAdapter mJavaAdapter; - @Mock private ShadeInteractor mShadeInteractor; - @Mock private DumpManager dumpManager; - private AvalancheController mAvalancheController; - - @Mock private Handler mBgHandler; - - private static final class TestableHeadsUpManagerPhone extends HeadsUpManagerPhone { - TestableHeadsUpManagerPhone( - Context context, - HeadsUpManagerLogger headsUpManagerLogger, - GroupMembershipManager groupManager, - VisualStabilityProvider visualStabilityProvider, - StatusBarStateController statusBarStateController, - KeyguardBypassController keyguardBypassController, - ConfigurationController configurationController, - GlobalSettings globalSettings, - SystemClock systemClock, - DelayableExecutor executor, - AccessibilityManagerWrapper accessibilityManagerWrapper, - UiEventLogger uiEventLogger, - JavaAdapter javaAdapter, - ShadeInteractor shadeInteractor, - AvalancheController avalancheController - ) { - super( - context, - headsUpManagerLogger, - statusBarStateController, - keyguardBypassController, - groupManager, - visualStabilityProvider, - configurationController, - mockExecutorHandler(executor), - globalSettings, - systemClock, - executor, - accessibilityManagerWrapper, - uiEventLogger, - javaAdapter, - shadeInteractor, - avalancheController - ); - mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME; - mAutoDismissTime = TEST_AUTO_DISMISS_TIME; - } - } - - private HeadsUpManagerPhone createHeadsUpManagerPhone() { - return new TestableHeadsUpManagerPhone( - mContext, - mHeadsUpManagerLogger, - mGroupManager, - mVSProvider, - mStatusBarStateController, - mBypassController, - mConfigurationController, - mGlobalSettings, - mSystemClock, - mExecutor, - mAccessibilityManagerWrapper, - mUiEventLogger, - mJavaAdapter, - mShadeInteractor, - mAvalancheController - ); - } - - @Parameters(name = "{0}") - public static List<FlagsParameterization> getFlags() { - return FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME); - } - - public HeadsUpManagerPhoneTest(FlagsParameterization flags) { - super(flags); - } - - @Before - public void setUp() { - when(mShadeInteractor.isAnyExpanded()).thenReturn(StateFlowKt.MutableStateFlow(false)); - final AccessibilityManagerWrapper accessibilityMgr = - mDependency.injectMockDependency(AccessibilityManagerWrapper.class); - when(accessibilityMgr.getRecommendedTimeoutMillis(anyInt(), anyInt())) - .thenReturn(TEST_AUTO_DISMISS_TIME); - when(mVSProvider.isReorderingAllowed()).thenReturn(true); - mDependency.injectMockDependency(NotificationShadeWindowController.class); - mContext.getOrCreateTestableResources().addOverride( - R.integer.ambient_notification_extension_time, 500); - - mAvalancheController = new AvalancheController(dumpManager, mUiEventLogger, mBgHandler); - } - - @Test - public void testSnooze() { - final HeadsUpManager hmp = createHeadsUpManagerPhone(); - final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext); - - hmp.showNotification(entry); - hmp.snooze(); - - assertTrue(hmp.isSnoozed(entry.getSbn().getPackageName())); - } - - @Test - public void testSwipedOutNotification() { - final HeadsUpManager hmp = createHeadsUpManagerPhone(); - final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext); - - hmp.showNotification(entry); - hmp.addSwipedOutNotification(entry.getKey()); - - // Remove should succeed because the notification is swiped out - final boolean removedImmediately = hmp.removeNotification(entry.getKey(), - /* releaseImmediately = */ false); - - assertTrue(removedImmediately); - assertFalse(hmp.isHeadsUpEntry(entry.getKey())); - } - - @Test - public void testCanRemoveImmediately_swipedOut() { - final HeadsUpManager hmp = createHeadsUpManagerPhone(); - final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext); - - hmp.showNotification(entry); - hmp.addSwipedOutNotification(entry.getKey()); - - // Notification is swiped so it can be immediately removed. - assertTrue(hmp.canRemoveImmediately(entry.getKey())); - } - - @Ignore("b/141538055") - @Test - public void testCanRemoveImmediately_notTopEntry() { - final HeadsUpManager hmp = createHeadsUpManagerPhone(); - final NotificationEntry earlierEntry = - HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext); - final NotificationEntry laterEntry = - HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext); - laterEntry.setRow(mRow); - - hmp.showNotification(earlierEntry); - hmp.showNotification(laterEntry); - - // Notification is "behind" a higher priority notification so we can remove it immediately. - assertTrue(hmp.canRemoveImmediately(earlierEntry.getKey())); - } - - @Test - public void testExtendHeadsUp() { - final HeadsUpManagerPhone hmp = createHeadsUpManagerPhone(); - final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext); - - hmp.showNotification(entry); - hmp.extendHeadsUp(); - mSystemClock.advanceTime(TEST_AUTO_DISMISS_TIME + hmp.mExtensionTime / 2); - - assertTrue(hmp.isHeadsUpEntry(entry.getKey())); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt new file mode 100644 index 000000000000..663cf1c44dad --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.policy + +import android.content.Context +import android.os.Handler +import android.platform.test.flag.junit.FlagsParameterization +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dump.DumpManager +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager +import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor +import com.android.systemui.statusbar.phone.ConfigurationControllerImpl +import com.android.systemui.statusbar.phone.HeadsUpManagerPhone +import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.concurrency.mockExecutorHandler +import com.android.systemui.util.kotlin.JavaAdapter +import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.time.SystemClock +import junit.framework.Assert +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +@RunWithLooper +class HeadsUpManagerPhoneTest(flags: FlagsParameterization) : BaseHeadsUpManagerTest(flags) { + + private val mHeadsUpManagerLogger = HeadsUpManagerLogger(logcatLogBuffer()) + + @Mock private lateinit var mGroupManager: GroupMembershipManager + + @Mock private lateinit var mVSProvider: VisualStabilityProvider + + @Mock private lateinit var mStatusBarStateController: StatusBarStateController + + @Mock private lateinit var mBypassController: KeyguardBypassController + + @Mock private lateinit var mConfigurationController: ConfigurationControllerImpl + + @Mock private lateinit var mAccessibilityManagerWrapper: AccessibilityManagerWrapper + + @Mock private lateinit var mUiEventLogger: UiEventLogger + + @Mock private lateinit var mJavaAdapter: JavaAdapter + + @Mock private lateinit var mShadeInteractor: ShadeInteractor + + @Mock private lateinit var dumpManager: DumpManager + private lateinit var mAvalancheController: AvalancheController + + @Mock private lateinit var mBgHandler: Handler + + private class TestableHeadsUpManagerPhone( + context: Context, + headsUpManagerLogger: HeadsUpManagerLogger, + groupManager: GroupMembershipManager, + visualStabilityProvider: VisualStabilityProvider, + statusBarStateController: StatusBarStateController, + keyguardBypassController: KeyguardBypassController, + configurationController: ConfigurationController, + globalSettings: GlobalSettings, + systemClock: SystemClock, + executor: DelayableExecutor, + accessibilityManagerWrapper: AccessibilityManagerWrapper, + uiEventLogger: UiEventLogger, + javaAdapter: JavaAdapter, + shadeInteractor: ShadeInteractor, + avalancheController: AvalancheController + ) : + HeadsUpManagerPhone( + context, + headsUpManagerLogger, + statusBarStateController, + keyguardBypassController, + groupManager, + visualStabilityProvider, + configurationController, + mockExecutorHandler(executor), + globalSettings, + systemClock, + executor, + accessibilityManagerWrapper, + uiEventLogger, + javaAdapter, + shadeInteractor, + avalancheController + ) { + init { + mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME + mAutoDismissTime = TEST_AUTO_DISMISS_TIME + } + } + + private fun createHeadsUpManagerPhone(): HeadsUpManagerPhone { + return TestableHeadsUpManagerPhone( + mContext, + mHeadsUpManagerLogger, + mGroupManager, + mVSProvider, + mStatusBarStateController, + mBypassController, + mConfigurationController, + mGlobalSettings, + mSystemClock, + mExecutor, + mAccessibilityManagerWrapper, + mUiEventLogger, + mJavaAdapter, + mShadeInteractor, + mAvalancheController + ) + } + + @Before + fun setUp() { + whenever(mShadeInteractor.isAnyExpanded).thenReturn(MutableStateFlow(false)) + whenever(mVSProvider.isReorderingAllowed).thenReturn(true) + val accessibilityMgr = + mDependency.injectMockDependency(AccessibilityManagerWrapper::class.java) + whenever( + accessibilityMgr.getRecommendedTimeoutMillis( + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt() + ) + ) + .thenReturn(TEST_AUTO_DISMISS_TIME) + mDependency.injectMockDependency(NotificationShadeWindowController::class.java) + mContext + .getOrCreateTestableResources() + .addOverride(R.integer.ambient_notification_extension_time, 500) + mAvalancheController = AvalancheController(dumpManager, mUiEventLogger, mBgHandler) + } + + @Test + fun testSnooze() { + val hmp: HeadsUpManager = createHeadsUpManagerPhone() + val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + hmp.showNotification(entry) + hmp.snooze() + Assert.assertTrue(hmp.isSnoozed(entry.sbn.packageName)) + } + + @Test + fun testSwipedOutNotification() { + val hmp: HeadsUpManager = createHeadsUpManagerPhone() + val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + hmp.showNotification(entry) + hmp.addSwipedOutNotification(entry.key) + + // Remove should succeed because the notification is swiped out + val removedImmediately = hmp.removeNotification(entry.key, /* releaseImmediately= */ false) + Assert.assertTrue(removedImmediately) + Assert.assertFalse(hmp.isHeadsUpEntry(entry.key)) + } + + @Test + fun testCanRemoveImmediately_swipedOut() { + val hmp: HeadsUpManager = createHeadsUpManagerPhone() + val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + hmp.showNotification(entry) + hmp.addSwipedOutNotification(entry.key) + + // Notification is swiped so it can be immediately removed. + Assert.assertTrue(hmp.canRemoveImmediately(entry.key)) + } + + @Ignore("b/141538055") + @Test + fun testCanRemoveImmediately_notTopEntry() { + val hmp: HeadsUpManager = createHeadsUpManagerPhone() + val earlierEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + val laterEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext) + laterEntry.row = mRow + hmp.showNotification(earlierEntry) + hmp.showNotification(laterEntry) + + // Notification is "behind" a higher priority notification so we can remove it immediately. + Assert.assertTrue(hmp.canRemoveImmediately(earlierEntry.key)) + } + + @Test + fun testExtendHeadsUp() { + val hmp = createHeadsUpManagerPhone() + val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + hmp.showNotification(entry) + hmp.extendHeadsUp() + mSystemClock.advanceTime((TEST_AUTO_DISMISS_TIME + hmp.mExtensionTime / 2).toLong()) + Assert.assertTrue(hmp.isHeadsUpEntry(entry.key)) + } + + companion object { + @get:Parameters(name = "{0}") + val flags: List<FlagsParameterization> + get() = buildList { + addAll(FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME)) + addAll( + FlagsParameterization.allCombinationsOf(NotificationsHeadsUpRefactor.FLAG_NAME) + ) + } + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialog.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialog.java index 9e5db73cf885..cd864026207b 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialog.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialog.java @@ -26,6 +26,7 @@ import com.android.systemui.plugins.annotations.ProvidesInterface; @DependsOn(target = Callback.class) public interface VolumeDialog extends Plugin { String ACTION = "com.android.systemui.action.PLUGIN_VOLUME"; + String ACTION_VOLUME_UNDO = "com.android.systemui.volume.ACTION_VOLUME_UNDO"; int VERSION = 1; void init(int windowType, Callback callback); diff --git a/packages/SystemUI/res/layout/app_clips_screenshot.xml b/packages/SystemUI/res/layout/app_clips_screenshot.xml index b09d35d46ca0..5191895549b6 100644 --- a/packages/SystemUI/res/layout/app_clips_screenshot.xml +++ b/packages/SystemUI/res/layout/app_clips_screenshot.xml @@ -60,6 +60,7 @@ android:layout_marginStart="16dp" android:checked="true" android:text="@string/backlinks_include_link" + android:textColor="?android:textColorSecondary" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/preview" app:layout_constraintStart_toEndOf="@id/cancel" @@ -74,6 +75,7 @@ android:drawablePadding="4dp" android:gravity="center" android:paddingHorizontal="8dp" + android:textColor="?android:textColorSecondary" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/preview" app:layout_constraintStart_toEndOf="@id/backlinks_include_data" diff --git a/packages/SystemUI/res/layout/screenshot.xml b/packages/SystemUI/res/layout/screenshot.xml deleted file mode 100644 index c134c8e2d339..000000000000 --- a/packages/SystemUI/res/layout/screenshot.xml +++ /dev/null @@ -1,40 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2011 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<com.android.systemui.screenshot.ScreenshotView - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/screenshot_frame" - android:theme="@style/FloatingOverlay" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:importantForAccessibility="no"> - <ImageView - android:id="@+id/screenshot_scrolling_scrim" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="gone" - android:clickable="true" - android:importantForAccessibility="no"/> - <ImageView - android:id="@+id/screenshot_flash" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="gone" - android:elevation="7dp" - android:src="@android:color/white"/> - <include layout="@layout/screenshot_static" - android:id="@+id/screenshot_static"/> -</com.android.systemui.screenshot.ScreenshotView> diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml index 12e226a47bb5..afd4fa7a5edd 100644 --- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml +++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml @@ -33,9 +33,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:singleLine="true" - android:scrollHorizontally="true" - android:ellipsize="marquee" + android:layout_marginEnd="@dimen/magnification_setting_view_item_horizontal_spacing" android:text="@string/accessibility_magnifier_size" android:textAppearance="@style/TextAppearance.MagnificationSetting.Title" android:focusable="true" @@ -120,9 +118,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:singleLine="true" - android:scrollHorizontally="true" - android:ellipsize="marquee" + android:layout_marginEnd="@dimen/magnification_setting_view_item_horizontal_spacing" android:text="@string/accessibility_allow_diagonal_scrolling" android:textAppearance="@style/TextAppearance.MagnificationSetting.Title" android:labelFor="@id/magnifier_horizontal_lock_switch" diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index ca55c2394203..0350cd7dab98 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -100,8 +100,8 @@ <!-- The color of the navigation bar icons. Need to be in sync with ic_sysbar_* --> <color name="navigation_bar_icon_color">#E5FFFFFF</color> - <color name="white">@*android:color/white</color> - <color name="black">@*android:color/black</color> + <color name="navigation_bar_home_handle_light_color">#EBffffff</color> + <color name="navigation_bar_home_handle_dark_color">#99000000</color> <!-- The shadow color for light navigation bar icons. --> <color name="nav_key_button_shadow_color">#30000000</color> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 84d5dcbae253..7f7e6347b2f7 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -56,7 +56,7 @@ enabled for OLED devices to reduce/prevent burn in on the navigation bar (because of the black background and static button placements) and disabled for all other devices to prevent wasting cpu cycles on the dimming animation --> - <bool name="config_navigation_bar_enable_auto_dim_no_visible_wallpaper">false</bool> + <bool name="config_navigation_bar_enable_auto_dim_no_visible_wallpaper">true</bool> <!-- The maximum number of tiles in the QuickQSPanel --> <integer name="quick_qs_panel_max_tiles">4</integer> @@ -104,7 +104,7 @@ <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" --> <string name="quick_settings_tiles_stock" translatable="false"> - internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices + internet,bt,flashlight,dnd,modes,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices </string> <!-- The tiles to display in QuickSettings --> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 40bdc3eb50f8..eda7bb0e7f6d 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1323,6 +1323,7 @@ <dimen name="magnifier_drag_handle_padding">3dp</dimen> <!-- Magnification settings panel --> <dimen name="magnification_setting_view_margin">24dp</dimen> + <dimen name="magnification_setting_view_item_horizontal_spacing">12dp</dimen> <dimen name="magnification_setting_text_size">18sp</dimen> <dimen name="magnification_setting_background_padding">24dp</dimen> <dimen name="magnification_setting_background_corner_radius">28dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c1e99db63e3a..52e5dea1cca4 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -716,6 +716,8 @@ <!-- QuickSettings: Do not disturb - Priority only [CHAR LIMIT=NONE] --> <!-- QuickSettings: Do not disturb - Alarms only [CHAR LIMIT=NONE] --> <!-- QuickSettings: Do not disturb - Total silence [CHAR LIMIT=NONE] --> + <!-- QuickSettings: Priority modes [CHAR LIMIT=NONE] --> + <string name="quick_settings_modes_label">Priority modes</string> <!-- QuickSettings: Bluetooth [CHAR LIMIT=NONE] --> <string name="quick_settings_bluetooth_label">Bluetooth</string> <!-- QuickSettings: Bluetooth (Multiple) [CHAR LIMIT=NONE] --> @@ -3597,6 +3599,10 @@ that shows the user which keyboard shortcuts they can use. The "App shortcuts" are for example "Open browser" or "Open calculator". [CHAR LIMIT=NONE] --> <string name="shortcut_helper_category_app_shortcuts">App shortcuts</string> + <!-- Default Title of the keyboard shortcut helper category for current app. The helper is a + component that shows the user which keyboard shortcuts they can use. The current app + shortcuts are shortcuts provided by the currently open app. [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_current_app_shortcuts">Current App</string> <!-- Title of the keyboard shortcut helper category "Accessibility". The helper is a component that shows the user which keyboard shortcuts they can use. The "Accessibility" shortcuts are for example "Turn on talkback". [CHAR LIMIT=NONE] --> @@ -3652,4 +3658,6 @@ <string name="home_controls_dream_label">Home Controls</string> <!-- Description for home control panel [CHAR LIMIT=67] --> <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string> + <!-- Label for volume undo action [CHAR LIMIT=NONE] --> + <string name="volume_undo_action">Undo</string> </resources> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 047578c43159..36912acbbdb9 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -622,14 +622,14 @@ <style name="DualToneLightTheme"> <item name="iconBackgroundColor">@color/light_mode_icon_color_dual_tone_background</item> <item name="fillColor">@color/light_mode_icon_color_dual_tone_fill</item> - <item name="singleToneColor">@color/white</item> - <item name="homeHandleColor">@color/white</item> + <item name="singleToneColor">@color/light_mode_icon_color_single_tone</item> + <item name="homeHandleColor">@color/navigation_bar_home_handle_light_color</item> </style> <style name="DualToneDarkTheme"> <item name="iconBackgroundColor">@color/dark_mode_icon_color_dual_tone_background</item> <item name="fillColor">@color/dark_mode_icon_color_dual_tone_fill</item> - <item name="singleToneColor">@color/black</item> - <item name="homeHandleColor">@color/black</item> + <item name="singleToneColor">@color/dark_mode_icon_color_single_tone</item> + <item name="homeHandleColor">@color/navigation_bar_home_handle_dark_color</item> </style> <style name="QSHeaderDarkTheme"> <item name="iconBackgroundColor">@color/dark_mode_qs_icon_color_dual_tone_background</item> @@ -648,7 +648,7 @@ <item name="singleToneColor">?android:attr/textColorPrimary</item> </style> <style name="ScreenPinningRequestTheme" parent="@*android:style/ThemeOverlay.DeviceDefault.Accent"> - <item name="singleToneColor">@color/white</item> + <item name="singleToneColor">@color/light_mode_icon_color_single_tone</item> </style> <style name="TextAppearance.Volume"> @@ -1604,7 +1604,6 @@ <item name="android:fontFamily">google-sans</item> <item name="android:textColor">?androidprv:attr/textColorPrimary</item> <item name="android:textSize">@dimen/magnification_setting_text_size</item> - <item name="android:singleLine">true</item> </style> <style name="TextAppearance.MagnificationSetting.EditButton"> diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml index ad09b466dd3e..c7029272db7d 100644 --- a/packages/SystemUI/res/values/tiles_states_strings.xml +++ b/packages/SystemUI/res/values/tiles_states_strings.xml @@ -85,6 +85,16 @@ <item>On</item> </string-array> + <!-- State names for modes (Priority modes) tile: unavailable, off, on. + This subtitle is shown when the tile is in that particular state but does not set its own + subtitle, so some of these may never appear on screen. They should still be translated as + if they could appear. [CHAR LIMIT=32] --> + <string-array name="tile_states_modes"> + <item>Unavailable</item> + <item>Off</item> + <item>On</item> + </string-array> + <!-- State names for flashlight tile: unavailable, off, on. This subtitle is shown when the tile is in that particular state but does not set its own subtitle, so some of these may never appear on screen. They should still be translated as diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml index fe61c46e341d..eb0aae9ebcf1 100644 --- a/packages/SystemUI/res/xml/large_screen_shade_header.xml +++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml @@ -33,6 +33,7 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:layout_marginStart="8dp" + app:layout_constraintBaseline_toBaselineOf="@id/clock" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/clock" app:layout_constraintTop_toTopOf="parent" /> diff --git a/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/2.json b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/2.json new file mode 100644 index 000000000000..f10d92a3fc0f --- /dev/null +++ b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/2.json @@ -0,0 +1,81 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "02e2da2d36e6955200edd5fb49e63c72", + "entities": [ + { + "tableName": "communal_widget_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `widget_id` INTEGER NOT NULL, `component_name` TEXT NOT NULL, `item_id` INTEGER NOT NULL, `user_serial_number` INTEGER NOT NULL DEFAULT -1)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "widgetId", + "columnName": "widget_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "component_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "item_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSerialNumber", + "columnName": "user_serial_number", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + } + }, + { + "tableName": "communal_item_rank_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rank` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rank", + "columnName": "rank", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '02e2da2d36e6955200edd5fb49e63c72')" + ] + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index a42f4c2dda4c..baf8f5aeba29 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -54,7 +54,6 @@ import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.window.StatusBarWindowController; -import com.android.systemui.tuner.TunablePadding.TunablePaddingService; import com.android.systemui.tuner.TunerService; import dagger.Lazy; @@ -129,7 +128,6 @@ public class Dependency { @Nullable @Inject Lazy<VolumeDialogController> mVolumeDialogController; @Inject Lazy<MetricsLogger> mMetricsLogger; - @Inject Lazy<TunablePaddingService> mTunablePaddingService; @Inject Lazy<UiOffloadThread> mUiOffloadThread; @Inject Lazy<LightBarController> mLightBarController; @Inject Lazy<OverviewProxyService> mOverviewProxyService; @@ -177,7 +175,6 @@ public class Dependency { mProviders.put(FragmentService.class, mFragmentService::get); mProviders.put(VolumeDialogController.class, mVolumeDialogController::get); mProviders.put(MetricsLogger.class, mMetricsLogger::get); - mProviders.put(TunablePaddingService.class, mTunablePaddingService::get); mProviders.put(UiOffloadThread.class, mUiOffloadThread::get); mProviders.put(LightBarController.class, mLightBarController::get); mProviders.put(OverviewProxyService.class, mOverviewProxyService::get); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java index b4530ace68d6..ed7062b5a335 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java @@ -26,6 +26,7 @@ import android.content.res.Configuration; import android.util.Range; import android.view.WindowManager; +import com.android.internal.accessibility.common.MagnificationConstants; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.util.settings.SecureSettings; @@ -40,7 +41,8 @@ import com.android.systemui.util.settings.SecureSettings; public class MagnificationSettingsController implements ComponentCallbacks { // It should be consistent with the value defined in WindowMagnificationGestureHandler. - private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(1.0f, 8.0f); + private static final Range<Float> A11Y_ACTION_SCALE_RANGE = + new Range<>(1.0f, MagnificationConstants.SCALE_MAX_VALUE); private final Context mContext; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index f2a68a8d9fd7..5f6f21a6845b 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -55,7 +55,6 @@ import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.Switch; -import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; @@ -90,7 +89,6 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private SeekBarWithIconButtonsView mZoomSeekbar; private LinearLayout mAllowDiagonalScrollingView; - private TextView mAllowDiagonalScrollingTitle; private Switch mAllowDiagonalScrollingSwitch; private LinearLayout mPanelView; private LinearLayout mSettingView; @@ -98,7 +96,6 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private ImageButton mMediumButton; private ImageButton mLargeButton; private Button mDoneButton; - private TextView mSizeTitle; private Button mEditButton; private ImageButton mFullScreenButton; private int mLastSelectedButtonIndex = MagnificationSize.NONE; @@ -522,11 +519,8 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mMediumButton = mSettingView.findViewById(R.id.magnifier_medium_button); mLargeButton = mSettingView.findViewById(R.id.magnifier_large_button); mDoneButton = mSettingView.findViewById(R.id.magnifier_done_button); - mSizeTitle = mSettingView.findViewById(R.id.magnifier_size_title); mEditButton = mSettingView.findViewById(R.id.magnifier_edit_button); mFullScreenButton = mSettingView.findViewById(R.id.magnifier_full_button); - mAllowDiagonalScrollingTitle = - mSettingView.findViewById(R.id.magnifier_horizontal_lock_title); mZoomSeekbar = mSettingView.findViewById(R.id.magnifier_zoom_slider); mZoomSeekbar.setMax((int) (mZoomSeekbar.getChangeMagnitude() @@ -550,8 +544,6 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mDoneButton.setOnClickListener(mButtonClickListener); mFullScreenButton.setOnClickListener(mButtonClickListener); mEditButton.setOnClickListener(mButtonClickListener); - mSizeTitle.setSelected(true); - mAllowDiagonalScrollingTitle.setSelected(true); mSettingView.setOnApplyWindowInsetsListener((v, insets) -> { // Adds a pending post check to avoiding redundant calculation because this callback @@ -578,6 +570,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest // CONFIG_FONT_SCALE: font size change // CONFIG_LOCALE: language change // CONFIG_DENSITY: display size change + mParams.width = getPanelWidth(mContext); mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext); boolean showSettingPanelAfterConfigChange = mIsVisible; @@ -660,9 +653,22 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mCallback.onEditMagnifierSizeMode(enable); } - private static LayoutParams createLayoutParams(Context context) { + private int getPanelWidth(Context context) { + // The magnification settings panel width is limited to the minimum of + // 1. display width + // 2. panel done button width + left and right padding + // So we can directly calculate the proper panel width at runtime + int displayWidth = mWindowManager.getCurrentWindowMetrics().getBounds().width(); + int contentWidth = context.getResources() + .getDimensionPixelSize(R.dimen.magnification_setting_button_done_width); + int padding = context.getResources() + .getDimensionPixelSize(R.dimen.magnification_setting_background_padding); + return Math.min(displayWidth, contentWidth + 2 * padding); + } + + private LayoutParams createLayoutParams(Context context) { final LayoutParams params = new LayoutParams( - LayoutParams.WRAP_CONTENT, + getPanelWidth(context), LayoutParams.WRAP_CONTENT, LayoutParams.TYPE_NAVIGATION_BAR_PANEL, LayoutParams.FLAG_NOT_FOCUSABLE, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 4a28d8b05661..27ded747fd55 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -515,7 +515,7 @@ class MenuViewLayer extends FrameLayout implements List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY)); if (!activities.isEmpty()) { - mContext.startActivity(intent); + mContext.startActivityAsUser(intent, UserHandle.CURRENT); mStatusBarManager.collapsePanels(); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index 961d6aa1b821..f041f4d5963f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -186,7 +186,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } @Override - public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { + public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { dismissDialogIfExists(); Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS); Bundle bundle = new Bundle(); @@ -198,7 +198,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } @Override - public void onDeviceItemOnClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { + public void onDeviceItemOnClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { CachedBluetoothDevice cachedBluetoothDevice = deviceItem.getCachedBluetoothDevice(); switch (deviceItem.getType()) { case ACTIVE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE -> @@ -226,8 +226,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, final int activePresetIndex = mPresetsController.getActivePresetIndex(); refreshPresetInfoAdapter(presetInfos, activePresetIndex); mPresetSpinner.setVisibility( - (activeHearingDevice != null && !mPresetInfoAdapter.isEmpty()) ? VISIBLE - : GONE); + (activeHearingDevice != null && activeHearingDevice.isConnectedHapClientDevice() + && !mPresetInfoAdapter.isEmpty()) ? VISIBLE : GONE); }); } @@ -303,6 +303,11 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, mLocalBluetoothManager.getEventManager().unregisterCallback(this); } + @VisibleForTesting + void setHearingDevicesPresetsController(HearingDevicesPresetsController controller) { + mPresetsController = controller; + } + private void setupDeviceListView(SystemUIDialog dialog) { mDeviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext())); mHearingDeviceItemList = getHearingDevicesList(); @@ -311,12 +316,15 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } private void setupPresetSpinner(SystemUIDialog dialog) { - mPresetsController = new HearingDevicesPresetsController(mProfileManager, mPresetCallback); + if (mPresetsController == null) { + mPresetsController = new HearingDevicesPresetsController(mProfileManager, + mPresetCallback); + } final CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice( mHearingDeviceItemList); mPresetsController.setActiveHearingDevice(activeHearingDevice); - mPresetInfoAdapter = new ArrayAdapter<String>(dialog.getContext(), + mPresetInfoAdapter = new ArrayAdapter<>(dialog.getContext(), R.layout.hearing_devices_preset_spinner_selected, R.id.hearing_devices_preset_option_text); mPresetInfoAdapter.setDropDownViewResource( @@ -350,7 +358,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, final int activePresetIndex = mPresetsController.getActivePresetIndex(); refreshPresetInfoAdapter(presetInfos, activePresetIndex); mPresetSpinner.setVisibility( - (activeHearingDevice != null && !mPresetInfoAdapter.isEmpty()) ? VISIBLE : GONE); + (activeHearingDevice != null && activeHearingDevice.isConnectedHapClientDevice() + && !mPresetInfoAdapter.isEmpty()) ? VISIBLE : GONE); } private void setupPairNewDeviceButton(SystemUIDialog dialog, @Visibility int visibility) { diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt index cdeeb6ff0b23..7abad1448318 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt @@ -21,6 +21,9 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.database.ContentObserver +import android.os.Handler +import android.provider.Settings.Secure.USER_SETUP_COMPLETE import com.android.systemui.CoreStartable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.communal.domain.interactor.CommunalInteractor @@ -29,6 +32,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject @SysUISingleton @@ -38,10 +42,15 @@ constructor( private val broadcastDispatcher: BroadcastDispatcher, private val communalInteractor: CommunalInteractor, @CommunalLog logBuffer: LogBuffer, + private val secureSettings: SecureSettings, + handler: Handler, ) : CoreStartable, BroadcastReceiver() { private val logger = Logger(logBuffer, TAG) + private var oldToNewWidgetIdMap = emptyMap<Int, Int>() + private var userSetupComplete = false + override fun start() { broadcastDispatcher.registerReceiver( receiver = this, @@ -73,8 +82,53 @@ constructor( return } - val oldToNewWidgetIdMap = oldIds.zip(newIds).toMap() - communalInteractor.restoreWidgets(oldToNewWidgetIdMap) + oldToNewWidgetIdMap = oldIds.zip(newIds).toMap() + + logger.i({ "On old to new widget ids mapping updated: $str1" }) { + str1 = oldToNewWidgetIdMap.toString() + } + + maybeRestoreWidgets() + + // Start observing if user setup is not complete + if (!userSetupComplete) { + startObservingUserSetupComplete() + } + } + + private val userSetupObserver = + object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean) { + maybeRestoreWidgets() + + // Stop observing once user setup is complete + if (userSetupComplete) { + stopObservingUserSetupComplete() + } + } + } + + private fun maybeRestoreWidgets() { + val newValue = secureSettings.getInt(USER_SETUP_COMPLETE) > 0 + + if (userSetupComplete != newValue) { + userSetupComplete = newValue + logger.i({ "User setup complete: $bool1" }) { bool1 = userSetupComplete } + } + + if (userSetupComplete && oldToNewWidgetIdMap.isNotEmpty()) { + logger.i("Starting to restore widgets") + communalInteractor.restoreWidgets(oldToNewWidgetIdMap.toMap()) + oldToNewWidgetIdMap = emptyMap() + } + } + + private fun startObservingUserSetupComplete() { + secureSettings.registerContentObserverSync(USER_SETUP_COMPLETE, userSetupObserver) + } + + private fun stopObservingUserSetupComplete() { + secureSettings.unregisterContentObserverSync(userSetupObserver) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt new file mode 100644 index 000000000000..78016c6664a1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal + +import com.android.systemui.CoreStartable +import com.android.systemui.communal.data.repository.CommunalMediaRepository +import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.flags.Flags +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@SysUISingleton +class CommunalOngoingContentStartable +@Inject +constructor( + @Background val bgScope: CoroutineScope, + private val communalInteractor: CommunalInteractor, + private val communalMediaRepository: CommunalMediaRepository, + private val communalSmartspaceRepository: CommunalSmartspaceRepository, + private val featureFlags: FeatureFlagsClassic, +) : CoreStartable { + + override fun start() { + if ( + !featureFlags.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) || + !com.android.systemui.Flags.communalHub() + ) { + return + } + + bgScope.launch { + communalInteractor.isCommunalEnabled.collect { enabled -> + if (enabled) { + communalMediaRepository.startListening() + communalSmartspaceRepository.startListening() + } else { + communalMediaRepository.stopListening() + communalSmartspaceRepository.stopListening() + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt index 88c3f9f6af2e..bde6f42b16af 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt @@ -18,7 +18,9 @@ package com.android.systemui.communal import android.provider.Settings import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.TransitionKey import com.android.systemui.CoreStartable +import com.android.systemui.Flags.communalHub import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.shared.model.CommunalScenes @@ -28,6 +30,8 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dock.DockManager +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState @@ -74,6 +78,7 @@ constructor( private val systemSettings: SystemSettings, centralSurfacesOpt: Optional<CentralSurfaces>, private val notificationShadeWindowController: NotificationShadeWindowController, + private val featureFlagsClassic: FeatureFlagsClassic, @Application private val applicationScope: CoroutineScope, @Background private val bgScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @@ -86,13 +91,21 @@ constructor( private val centralSurfaces: CentralSurfaces? by centralSurfacesOpt + private val flagEnabled: Boolean by lazy { + featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub() + } + override fun start() { + if (!flagEnabled) { + return + } + // Handle automatically switching based on keyguard state. keyguardTransitionInteractor.startedKeyguardTransitionStep .mapLatest(::determineSceneAfterTransition) .filterNotNull() - .onEach { nextScene -> - communalSceneInteractor.changeScene(nextScene, CommunalTransitionKeys.SimpleFade) + .onEach { (nextScene, nextTransition) -> + communalSceneInteractor.changeScene(nextScene, nextTransition) } .launchIn(applicationScope) @@ -188,7 +201,7 @@ constructor( private suspend fun determineSceneAfterTransition( lastStartedTransition: TransitionStep, - ): SceneKey? { + ): Pair<SceneKey, TransitionKey>? { val to = lastStartedTransition.to val from = lastStartedTransition.from val docked = dockManager.isDocked @@ -201,22 +214,27 @@ constructor( // underneath the hub is shown. When launching activities over lockscreen, we only // change scenes once the activity launch animation is finished, so avoid // changing the scene here. - CommunalScenes.Blank + Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade) } to == KeyguardState.GLANCEABLE_HUB && from == KeyguardState.OCCLUDED -> { // When transitioning to the hub from an occluded state, fade out the hub without // doing any translation. - CommunalScenes.Communal + Pair(CommunalScenes.Communal, CommunalTransitionKeys.SimpleFade) } // Transitioning to Blank scene when entering the edit mode will be handled separately // with custom animations. to == KeyguardState.GONE && !communalInteractor.editModeOpen.value -> - CommunalScenes.Blank + Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade) !docked && !KeyguardState.deviceIsAwakeInState(to) -> { // If the user taps the screen and wakes the device within this timeout, we don't // want to dismiss the hub delay(AWAKE_DEBOUNCE_DELAY) - CommunalScenes.Blank + Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade) + } + from == KeyguardState.DOZING && to == KeyguardState.GLANCEABLE_HUB -> { + // Make sure the communal hub is showing (immediately, not fading in) when + // transitioning from dozing to hub. + Pair(CommunalScenes.Communal, CommunalTransitionKeys.Immediately) } else -> null } diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index 2406cc6bcea2..3d201a36417b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -23,6 +23,7 @@ import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModu import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryModule import com.android.systemui.communal.data.repository.CommunalRepositoryModule import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule +import com.android.systemui.communal.data.repository.CommunalSmartspaceRepositoryModule import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule import com.android.systemui.communal.shared.model.CommunalScenes @@ -52,6 +53,8 @@ import kotlinx.coroutines.CoroutineScope CommunalWidgetModule::class, CommunalPrefsRepositoryModule::class, CommunalSettingsRepositoryModule::class, + CommunalSmartspaceRepositoryModule::class, + CommunalStartableModule::class, ] ) interface CommunalModule { diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt new file mode 100644 index 000000000000..74a2cd3f22c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.dagger + +import com.android.systemui.CoreStartable +import com.android.systemui.communal.CommunalBackupRestoreStartable +import com.android.systemui.communal.CommunalDreamStartable +import com.android.systemui.communal.CommunalOngoingContentStartable +import com.android.systemui.communal.CommunalSceneStartable +import com.android.systemui.communal.log.CommunalLoggerStartable +import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface CommunalStartableModule { + @Binds + @IntoMap + @ClassKey(CommunalLoggerStartable::class) + fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(CommunalSceneStartable::class) + fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(CommunalDreamStartable::class) + fun bindCommunalDreamStartable(impl: CommunalDreamStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(CommunalAppWidgetHostStartable::class) + fun bindCommunalAppWidgetHostStartable(impl: CommunalAppWidgetHostStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(CommunalBackupRestoreStartable::class) + fun bindCommunalBackupRestoreStartable(impl: CommunalBackupRestoreStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(CommunalOngoingContentStartable::class) + fun bindCommunalOngoingContentStartable(impl: CommunalOngoingContentStartable): CoreStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt b/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt index a8e5174494a1..c3d2683ce953 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt @@ -43,11 +43,13 @@ class CommunalBackupUtils( val widgetsFromDb = runBlocking { database.communalWidgetDao().getWidgets().first() } val widgetsState = mutableListOf<CommunalHubState.CommunalWidgetItem>() widgetsFromDb.keys.forEach { rankItem -> + val widget = widgetsFromDb[rankItem]!! widgetsState.add( CommunalHubState.CommunalWidgetItem().apply { rank = rankItem.rank - widgetId = widgetsFromDb[rankItem]!!.widgetId - componentName = widgetsFromDb[rankItem]?.componentName + widgetId = widget.widgetId + componentName = widget.componentName + userSerialNumber = widget.userSerialNumber } ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt index 3ce81094b696..dff63527ba05 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt @@ -17,17 +17,21 @@ package com.android.systemui.communal.data.db import android.content.Context +import android.util.Log import androidx.annotation.VisibleForTesting import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import com.android.systemui.res.R -@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 1) +@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 2) abstract class CommunalDatabase : RoomDatabase() { abstract fun communalWidgetDao(): CommunalWidgetDao companion object { + private const val TAG = "CommunalDatabase" private var instance: CommunalDatabase? = null /** @@ -51,7 +55,8 @@ abstract class CommunalDatabase : RoomDatabase() { context.resources.getString(R.string.config_communalDatabase) ) .also { builder -> - builder.fallbackToDestructiveMigration(dropAllTables = false) + builder.addMigrations(MIGRATION_1_2) + builder.fallbackToDestructiveMigration(dropAllTables = true) callback?.let { callback -> builder.addCallback(callback) } } .build() @@ -64,5 +69,23 @@ abstract class CommunalDatabase : RoomDatabase() { fun setInstance(database: CommunalDatabase) { instance = database } + + /** + * This migration adds a user_serial_number column and sets its default value as + * [CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED]. Work profile widgets added before the + * migration still work as expected, but they would be backed up as personal. + */ + @VisibleForTesting + val MIGRATION_1_2 = + object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i(TAG, "Migrating from version 1 to 2") + db.execSQL( + "ALTER TABLE communal_widget_table " + + "ADD COLUMN user_serial_number INTEGER NOT NULL DEFAULT " + + "${CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED}" + ) + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt index 0d5336ab8540..e33aead11842 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt @@ -29,7 +29,28 @@ data class CommunalWidgetItem( @ColumnInfo(name = "component_name") val componentName: String, /** Reference the id of an item persisted in the glanceable hub */ @ColumnInfo(name = "item_id") val itemId: Long, -) + /** + * A serial number of the user that the widget provider is associated with. For example, a work + * profile widget. + * + * A serial number may be different from its user id in that user ids may be recycled but serial + * numbers are unique until the device is wiped. + * + * Most commonly, this value would be 0 for the primary user, and 10 for the work profile. + */ + @ColumnInfo(name = "user_serial_number", defaultValue = "$USER_SERIAL_NUMBER_UNDEFINED") + val userSerialNumber: Int, +) { + companion object { + /** + * The user serial number associated with the widget is undefined. + * + * This should only happen for widgets migrated from V1 before user serial number was + * included in the schema. + */ + const val USER_SERIAL_NUMBER_UNDEFINED = -1 + } +} @Entity(tableName = "communal_item_rank_table") data class CommunalItemRank( diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt index d174fd1c97ea..4dcd9bfd361b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal.data.db import android.content.ComponentName +import android.os.UserManager import androidx.room.Dao import androidx.room.Delete import androidx.room.Query @@ -53,23 +54,34 @@ constructor( private val communalWidgetDaoProvider: Provider<CommunalWidgetDao>, @Named(DEFAULT_WIDGETS) private val defaultWidgets: Array<String>, @CommunalLog logBuffer: LogBuffer, + private val userManager: UserManager, ) : RoomDatabase.Callback() { companion object { private const val TAG = "DefaultWidgetPopulation" } + private val logger = Logger(logBuffer, TAG) override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) - applicationScope.launch { - addDefaultWidgets() - logger.i("Default widgets were populated in the database.") - } + applicationScope.launch { addDefaultWidgets() } } // Read default widgets from config.xml and populate the database. private suspend fun addDefaultWidgets() = withContext(bgDispatcher) { + // Default widgets should be associated with the main user. + val userSerialNumber = + userManager.mainUser?.let { mainUser -> + userManager.getUserSerialNumber(mainUser.identifier) + } + if (userSerialNumber == null) { + logger.w( + "Skipped populating default widgets because device does not have a main user" + ) + return@withContext + } + defaultWidgets.forEachIndexed { index, name -> val provider = ComponentName.unflattenFromString(name) provider?.let { @@ -80,11 +92,14 @@ constructor( .addWidget( widgetId = id, provider = provider, - priority = defaultWidgets.size - index + priority = defaultWidgets.size - index, + userSerialNumber = userSerialNumber, ) } } } + + logger.i("Populated default widgets in the database.") } } @@ -106,10 +121,16 @@ interface CommunalWidgetDao { fun deleteItemRankById(itemId: Long) @Query( - "INSERT INTO communal_widget_table(widget_id, component_name, item_id) " + - "VALUES(:widgetId, :componentName, :itemId)" + "INSERT INTO communal_widget_table" + + "(widget_id, component_name, item_id, user_serial_number) " + + "VALUES(:widgetId, :componentName, :itemId, :userSerialNumber)" ) - fun insertWidget(widgetId: Int, componentName: String, itemId: Long): Long + fun insertWidget( + widgetId: Int, + componentName: String, + itemId: Long, + userSerialNumber: Int, + ): Long @Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)") fun insertItemRank(rank: Int): Long @@ -132,28 +153,41 @@ interface CommunalWidgetDao { } @Transaction - fun addWidget(widgetId: Int, provider: ComponentName, priority: Int): Long { + fun addWidget( + widgetId: Int, + provider: ComponentName, + priority: Int, + userSerialNumber: Int, + ): Long { return addWidget( widgetId = widgetId, componentName = provider.flattenToString(), priority = priority, + userSerialNumber = userSerialNumber, ) } @Transaction - fun addWidget(widgetId: Int, componentName: String, priority: Int): Long { + fun addWidget( + widgetId: Int, + componentName: String, + priority: Int, + userSerialNumber: Int, + ): Long { return insertWidget( widgetId = widgetId, componentName = componentName, itemId = insertItemRank(priority), + userSerialNumber = userSerialNumber, ) } @Transaction fun deleteWidgetById(widgetId: Int): Boolean { val widget = - getWidgetByIdNow(widgetId) ?: // no entry to delete from db - return false + getWidgetByIdNow(widgetId) + ?: // no entry to delete from db + return false deleteItemRankById(widget.itemId) deleteWidgets(widget) @@ -166,6 +200,8 @@ interface CommunalWidgetDao { clearCommunalWidgetsTable() clearCommunalItemRankTable() - state.widgets.forEach { addWidget(it.widgetId, it.componentName, it.rank) } + state.widgets.forEach { + addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalSmartspaceTimer.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalSmartspaceTimer.kt new file mode 100644 index 000000000000..ff9dddd7c516 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalSmartspaceTimer.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.model + +import android.widget.RemoteViews + +/** Data model of a smartspace timer in the Glanceable Hub. */ +data class CommunalSmartspaceTimer( + /** Unique id that identifies the timer. */ + val smartspaceTargetId: String, + /** Timestamp in milliseconds of when the timer was created. */ + val createdTimestampMillis: Long, + /** Remote views for the timer that is rendered in Glanceable Hub. */ + val remoteViews: RemoteViews, +) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt index e5a0e5070b94..fe9154c192c5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt @@ -30,6 +30,12 @@ import kotlinx.coroutines.flow.MutableStateFlow /** Encapsulates the state of smartspace in communal. */ interface CommunalMediaRepository { val mediaModel: Flow<CommunalMediaModel> + + /** Start listening for media updates. */ + fun startListening() + + /** Stop listening for media updates. */ + fun stopListening() } @SysUISingleton @@ -38,29 +44,7 @@ class CommunalMediaRepositoryImpl constructor( private val mediaDataManager: MediaDataManager, @CommunalTableLog tableLogBuffer: TableLogBuffer, -) : CommunalMediaRepository { - - private val mediaDataListener = - object : MediaDataManager.Listener { - override fun onMediaDataLoaded( - key: String, - oldKey: String?, - data: MediaData, - immediately: Boolean, - receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean - ) { - updateMediaModel(data) - } - - override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { - updateMediaModel() - } - } - - init { - mediaDataManager.addListener(mediaDataListener) - } +) : CommunalMediaRepository, MediaDataManager.Listener { private val _mediaModel: MutableStateFlow<CommunalMediaModel> = MutableStateFlow(CommunalMediaModel.INACTIVE) @@ -72,6 +56,29 @@ constructor( initialValue = CommunalMediaModel.INACTIVE, ) + override fun startListening() { + mediaDataManager.addListener(this) + } + + override fun stopListening() { + mediaDataManager.removeListener(this) + } + + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean, + receivedSmartspaceCardLatency: Int, + isSsReactivated: Boolean + ) { + updateMediaModel(data) + } + + override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { + updateMediaModel() + } + private fun updateMediaModel(data: MediaData? = null) { if (mediaDataManager.hasActiveMediaOrRecommendation()) { _mediaModel.value = diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt new file mode 100644 index 000000000000..e1d9bef67490 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import android.app.smartspace.SmartspaceTarget +import android.os.Parcelable +import androidx.annotation.VisibleForTesting +import com.android.systemui.communal.data.model.CommunalSmartspaceTimer +import com.android.systemui.communal.smartspace.CommunalSmartspaceController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.util.time.SystemClock +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +interface CommunalSmartspaceRepository { + /** Smartspace timer targets for the communal surface. */ + val timers: Flow<List<CommunalSmartspaceTimer>> + + /** Start listening for smartspace updates. */ + fun startListening() + + /** Stop listening for smartspace updates. */ + fun stopListening() +} + +@SysUISingleton +class CommunalSmartspaceRepositoryImpl +@Inject +constructor( + private val communalSmartspaceController: CommunalSmartspaceController, + @Main private val uiExecutor: Executor, + private val systemClock: SystemClock, +) : CommunalSmartspaceRepository, BcSmartspaceDataPlugin.SmartspaceTargetListener { + + private val _timers: MutableStateFlow<List<CommunalSmartspaceTimer>> = + MutableStateFlow(emptyList()) + override val timers: Flow<List<CommunalSmartspaceTimer>> = _timers + + private var targetCreationTimes = emptyMap<String, Long>() + + override fun onSmartspaceTargetsUpdated(targetsNullable: MutableList<out Parcelable>?) { + val targets = targetsNullable?.filterIsInstance<SmartspaceTarget>() ?: emptyList() + val timerTargets = + targets + .filter { target -> + target.featureType == SmartspaceTarget.FEATURE_TIMER && + target.remoteViews != null + } + .associateBy { stableId(it.smartspaceTargetId) } + + // The creation times from smartspace targets are unreliable (b/318535930). Therefore, + // SystemUI uses the timestamp of which a timer first appears, and caches these values to + // prevent timers from swapping positions in the hub. + targetCreationTimes = + timerTargets.mapValues { (stableId, _) -> + targetCreationTimes[stableId] ?: systemClock.currentTimeMillis() + } + + _timers.value = + timerTargets.map { (stableId, target) -> + CommunalSmartspaceTimer( + // The view layer should have the instance based smartspaceTargetId instead of + // stable id, so that when a new instance of the timer is created, for example, + // when it is paused, the view should re-render its remote views. + smartspaceTargetId = target.smartspaceTargetId, + createdTimestampMillis = targetCreationTimes[stableId]!!, + remoteViews = target.remoteViews!!, + ) + } + } + + override fun startListening() { + if (android.app.smartspace.flags.Flags.remoteViews()) { + uiExecutor.execute { + communalSmartspaceController.addListener( + listener = this@CommunalSmartspaceRepositoryImpl + ) + } + } + } + + override fun stopListening() { + uiExecutor.execute { + communalSmartspaceController.removeListener( + listener = this@CommunalSmartspaceRepositoryImpl + ) + } + } + + companion object { + /** + * The smartspace target id is instance-based, meaning a single timer (from the user's + * perspective) can have multiple instances. For example, when a timer is paused, a new + * instance is created. To address this, SystemUI manually removes the instance id to + * maintain a consistent id across sessions. + * + * It is assumed that timer target ids follow this format: timer-${stableId}-${instanceId}. + * This function returns timer-${stableId}, stripping out the instance id. + */ + @VisibleForTesting + fun stableId(targetId: String): String { + return targetId.split("-").take(2).joinToString("-") + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryModule.kt index c77bcc50b69a..b11c6d6d5d55 100644 --- a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,15 @@ * limitations under the License. */ -package com.android.systemui.smartspace.data.repository +package com.android.systemui.communal.data.repository import dagger.Binds import dagger.Module @Module -interface SmartspaceRepositoryModule { - @Binds fun smartspaceRepository(impl: SmartspaceRepositoryImpl): SmartspaceRepository +interface CommunalSmartspaceRepositoryModule { + @Binds + fun communalSmartspaceRepository( + impl: CommunalSmartspaceRepositoryImpl + ): CommunalSmartspaceRepository } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index fdb797d5ba06..ab4c9d20b368 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -20,10 +20,12 @@ import android.app.backup.BackupManager import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.os.UserHandle +import android.os.UserManager import com.android.systemui.common.data.repository.PackageChangeRepository import com.android.systemui.common.shared.model.PackageInstallSession import com.android.systemui.communal.data.backup.CommunalBackupUtils import com.android.systemui.communal.data.db.CommunalWidgetDao +import com.android.systemui.communal.data.db.CommunalWidgetItem import com.android.systemui.communal.nano.CommunalHubState import com.android.systemui.communal.proto.toCommunalHubState import com.android.systemui.communal.shared.model.CommunalWidgetContentModel @@ -98,6 +100,7 @@ constructor( private val backupManager: BackupManager, private val backupUtils: CommunalBackupUtils, packageChangeRepository: PackageChangeRepository, + private val userManager: UserManager, ) : CommunalWidgetRepository { companion object { const val TAG = "CommunalWidgetRepository" @@ -185,6 +188,7 @@ constructor( widgetId = id, provider = provider, priority = priority, + userSerialNumber = userManager.getUserSerialNumber(user.identifier), ) backupManager.dataChanged() } else { @@ -228,9 +232,38 @@ constructor( return@launch } + // Abort restoring widgets if this code is somehow run on a device that does not have + // a main user, e.g. auto. + val mainUser = userManager.mainUser + if (mainUser == null) { + logger.w("Skipped restoring widgets because device does not have a main user") + return@launch + } + val widgetsWithHost = appWidgetHost.appWidgetIds.toList() val widgetsToRemove = widgetsWithHost.toMutableList() + val oldUserSerialNumbers = state.widgets.map { it.userSerialNumber }.distinct() + val usersMap = + oldUserSerialNumbers.associateWith { oldUserSerialNumber -> + if (oldUserSerialNumber == CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED) { + // If user serial number from the backup is undefined, the widget was added + // to the hub before user serial numbers are stored in the database. In this + // case, we restore the widget with the main user. + mainUser + } else { + // If the user serial number is defined, look up whether the user is + // restored. This API returns a user handle matching its backed up user + // serial number, if the user is restored. Otherwise, null is returned. + backupManager.getUserForAncestralSerialNumber(oldUserSerialNumber.toLong()) + ?: null + } + } + logger.d({ "Restored users map: $str1" }) { str1 = usersMap.toString() } + + // A set to hold all widgets that belong to non-main users + val secondaryUserWidgets = mutableSetOf<CommunalHubState.CommunalWidgetItem>() + // Produce a new state to be restored, skipping invalid widgets val newWidgets = state.widgets.mapNotNull { restoredWidget -> @@ -249,20 +282,64 @@ constructor( return@mapNotNull null } + // Skip if user / profile is not registered + val newUser = usersMap[restoredWidget.userSerialNumber] + if (newUser == null) { + logger.d({ + "Skipped restoring widget $int1 because its user $int2 is not " + + "registered" + }) { + int1 = restoredWidget.widgetId + int2 = restoredWidget.userSerialNumber + } + return@mapNotNull null + } + + // Place secondary user widgets in a bucket to be manually bound later because + // of a platform bug (b/349852237) that backs up work profile widgets as + // personal. + if (newUser.identifier != mainUser.identifier) { + logger.d({ + "Skipped restoring widget $int1 for now because its new user $int2 " + + "is secondary. This widget will be bound later." + }) { + int1 = restoredWidget.widgetId + int2 = newUser.identifier + } + secondaryUserWidgets.add(restoredWidget) + return@mapNotNull null + } + widgetsToRemove.remove(newWidgetId) CommunalHubState.CommunalWidgetItem().apply { widgetId = newWidgetId componentName = restoredWidget.componentName rank = restoredWidget.rank + userSerialNumber = userManager.getUserSerialNumber(newUser.identifier) } } val newState = CommunalHubState().apply { widgets = newWidgets.toTypedArray() } // Restore database - logger.i("Restoring communal database $newState") + logger.i("Restoring communal database:\n$newState") communalWidgetDao.restoreCommunalHubState(newState) + // Manually bind each secondary user widget due to platform bug b/349852237 + secondaryUserWidgets.forEach { widget -> + val newUser = usersMap[widget.userSerialNumber]!! + logger.i({ "Binding secondary user ($int1) widget $int2: $str1" }) { + int1 = newUser.identifier + int2 = widget.widgetId + str1 = widget.componentName + } + addWidget( + provider = ComponentName.unflattenFromString(widget.componentName)!!, + user = newUser, + priority = widget.rank, + ) + } + // Delete restored state file from disk backupUtils.clear() diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 86f5fe1cac57..597a2ce06692 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.communal.domain.interactor -import android.app.smartspace.SmartspaceTarget import android.content.ComponentName import android.content.Intent import android.content.IntentFilter @@ -29,6 +28,7 @@ import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.communal.data.repository.CommunalMediaRepository +import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent @@ -60,7 +60,6 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.UserTracker -import com.android.systemui.smartspace.data.repository.SmartspaceRepository import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.emitOnStart @@ -82,7 +81,6 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -101,7 +99,7 @@ constructor( private val widgetRepository: CommunalWidgetRepository, private val communalPrefsInteractor: CommunalPrefsInteractor, private val mediaRepository: CommunalMediaRepository, - smartspaceRepository: SmartspaceRepository, + private val smartspaceRepository: CommunalSmartspaceRepository, keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, communalSettingsInteractor: CommunalSettingsInteractor, @@ -435,19 +433,6 @@ constructor( } } - /** A flow of available smartspace targets. Currently only showing timers. */ - private val smartspaceTargets: Flow<List<SmartspaceTarget>> = - if (!smartspaceRepository.isSmartspaceRemoteViewsEnabled) { - flowOf(emptyList()) - } else { - smartspaceRepository.communalSmartspaceTargets.map { targets -> - targets.filter { target -> - target.featureType == SmartspaceTarget.FEATURE_TIMER && - target.remoteViews != null - } - } - } - /** CTA tile to be displayed in the glanceable hub (view mode). */ val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> = communalPrefsInteractor.isCtaDismissed.map { isDismissed -> @@ -472,16 +457,16 @@ constructor( * sized dynamically. */ fun getOngoingContent(mediaHostVisible: Boolean): Flow<List<CommunalContentModel.Ongoing>> = - combine(smartspaceTargets, mediaRepository.mediaModel) { smartspace, media -> + combine(smartspaceRepository.timers, mediaRepository.mediaModel) { timers, media -> val ongoingContent = mutableListOf<CommunalContentModel.Ongoing>() - // Add smartspace + // Add smartspace timers ongoingContent.addAll( - smartspace.map { target -> + timers.map { timer -> CommunalContentModel.Smartspace( - smartspaceTargetId = target.smartspaceTargetId, - remoteViews = target.remoteViews!!, - createdTimestampMillis = target.creationTimeMillis, + smartspaceTargetId = timer.smartspaceTargetId, + remoteViews = timer.remoteViews, + createdTimestampMillis = timer.createdTimestampMillis, ) } ) diff --git a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto index 08162593b4d5..bc14ae1eaff4 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto +++ b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto @@ -35,5 +35,8 @@ message CommunalHubState { // Rank or order of the widget in the communal hub. int32 rank = 3; + + // Serial number of the user associated with the widget. + int32 user_serial_number = 4; } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt index 2000f96bcdb0..684303ae73cc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt @@ -43,12 +43,6 @@ interface CommunalWidgetModule { @SysUISingleton @Provides - fun provideAppWidgetManager(@Application context: Context): Optional<AppWidgetManager> { - return Optional.ofNullable(AppWidgetManager.getInstance(context)) - } - - @SysUISingleton - @Provides fun provideCommunalAppWidgetHost( @Application context: Context, @Background backgroundScope: CoroutineScope, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index b0fc60e74b52..2ea27b76d81b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -16,6 +16,9 @@ package com.android.systemui.dagger; +import static com.android.systemui.Flags.enableViewCaptureTracing; +import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; + import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; @@ -39,6 +42,7 @@ import android.app.job.JobScheduler; import android.app.role.RoleManager; import android.app.smartspace.SmartspaceManager; import android.app.trust.TrustManager; +import android.appwidget.AppWidgetManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.companion.virtual.VirtualDeviceManager; @@ -111,6 +115,9 @@ import android.view.textclassifier.TextClassificationManager; import androidx.asynclayoutinflater.view.AsyncLayoutInflater; import androidx.core.app.NotificationManagerCompat; +import com.android.app.viewcapture.ViewCapture; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; +import com.android.app.viewcapture.ViewCaptureFactory; import com.android.internal.app.IBatteryStats; import com.android.internal.appwidget.IAppWidgetService; import com.android.internal.jank.InteractionJankMonitor; @@ -125,6 +132,7 @@ import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.user.utils.UserScopedService; import com.android.systemui.user.utils.UserScopedServiceImpl; +import dagger.Lazy; import dagger.Module; import dagger.Provides; @@ -367,6 +375,12 @@ public class FrameworkServicesModule { @Provides @Singleton + static Optional<AppWidgetManager> provideAppWidgetManager(@Application Context context) { + return Optional.ofNullable(AppWidgetManager.getInstance(context)); + } + + @Provides + @Singleton static IAppWidgetService provideIAppWidgetService() { return IAppWidgetService.Stub.asInterface( ServiceManager.getService(Context.APPWIDGET_SERVICE)); @@ -680,6 +694,15 @@ public class FrameworkServicesModule { @Provides @Singleton + static ViewCaptureAwareWindowManager provideViewCaptureAwareWindowManager( + WindowManager windowManager, Lazy<ViewCapture> daggerLazyViewCapture) { + return new ViewCaptureAwareWindowManager(windowManager, + /* lazyViewCapture= */ toKotlinLazy(daggerLazyViewCapture), + /* isViewCaptureEnabled= */ enableViewCaptureTracing()); + } + + @Provides + @Singleton static PermissionManager providePermissionManager(Context context) { PermissionManager pm = context.getSystemService(PermissionManager.class); if (pm != null) { @@ -764,4 +787,10 @@ public class FrameworkServicesModule { return IDeviceIdleController.Stub.asInterface( ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); } + + @Provides + @Singleton + static ViewCapture provideViewCapture(Context context) { + return ViewCaptureFactory.getInstance(context); + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 593196c54dae..88601dab891d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -24,11 +24,6 @@ import com.android.systemui.accessibility.Magnification import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.BiometricNotificationService import com.android.systemui.clipboardoverlay.ClipboardListener -import com.android.systemui.communal.CommunalDreamStartable -import com.android.systemui.communal.CommunalBackupRestoreStartable -import com.android.systemui.communal.CommunalSceneStartable -import com.android.systemui.communal.log.CommunalLoggerStartable -import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable import com.android.systemui.controls.dagger.StartControlsStartableModule import com.android.systemui.dagger.qualifiers.PerUser import com.android.systemui.dreams.AssistantAttentionMonitor @@ -80,12 +75,13 @@ import dagger.multibindings.IntoMap * @deprecated */ @Module( - includes = [ - MultiUserUtilsModule::class, - StartControlsStartableModule::class, - StartBinderLoggerModule::class, - WallpaperModule::class, - ] + includes = + [ + MultiUserUtilsModule::class, + StartControlsStartableModule::class, + StartBinderLoggerModule::class, + WallpaperModule::class, + ] ) abstract class SystemUICoreStartableModule { /** Inject into BiometricNotificationService */ @@ -96,25 +92,25 @@ abstract class SystemUICoreStartableModule { service: BiometricNotificationService ): CoreStartable - /** Inject into ClipboardListener. */ + /** Inject into ClipboardListener. */ @Binds @IntoMap @ClassKey(ClipboardListener::class) abstract fun bindClipboardListener(sysui: ClipboardListener): CoreStartable - /** Inject into GlobalActionsComponent. */ + /** Inject into GlobalActionsComponent. */ @Binds @IntoMap @ClassKey(GlobalActionsComponent::class) abstract fun bindGlobalActionsComponent(sysui: GlobalActionsComponent): CoreStartable - /** Inject into InstantAppNotifier. */ + /** Inject into InstantAppNotifier. */ @Binds @IntoMap @ClassKey(InstantAppNotifier::class) abstract fun bindInstantAppNotifier(sysui: InstantAppNotifier): CoreStartable - /** Inject into KeyboardUI. */ + /** Inject into KeyboardUI. */ @Binds @IntoMap @ClassKey(KeyboardUI::class) @@ -125,7 +121,7 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(MediaProjectionTaskSwitcherCoreStartable::class) abstract fun bindProjectedTaskListener( - sysui: MediaProjectionTaskSwitcherCoreStartable + sysui: MediaProjectionTaskSwitcherCoreStartable ): CoreStartable /** Inject into KeyguardBiometricLockoutLogger */ @@ -136,38 +132,38 @@ abstract class SystemUICoreStartableModule { sysui: KeyguardBiometricLockoutLogger ): CoreStartable - /** Inject into KeyguardViewMediator. */ + /** Inject into KeyguardViewMediator. */ @Binds @IntoMap @ClassKey(KeyguardViewMediator::class) abstract fun bindKeyguardViewMediator(sysui: KeyguardViewMediator): CoreStartable - /** Inject into LatencyTests. */ + /** Inject into LatencyTests. */ @Binds @IntoMap @ClassKey(LatencyTester::class) abstract fun bindLatencyTester(sysui: LatencyTester): CoreStartable - /** Inject into DisplaySwitchLatencyTracker. */ + /** Inject into DisplaySwitchLatencyTracker. */ @Binds @IntoMap @ClassKey(DisplaySwitchLatencyTracker::class) abstract fun bindDisplaySwitchLatencyTracker(sysui: DisplaySwitchLatencyTracker): CoreStartable - /** Inject into NotificationChannels. */ + /** Inject into NotificationChannels. */ @Binds @IntoMap @ClassKey(NotificationChannels::class) @PerUser abstract fun bindNotificationChannels(sysui: NotificationChannels): CoreStartable - /** Inject into ImmersiveModeConfirmation. */ + /** Inject into ImmersiveModeConfirmation. */ @Binds @IntoMap @ClassKey(ImmersiveModeConfirmation::class) abstract fun bindImmersiveModeConfirmation(sysui: ImmersiveModeConfirmation): CoreStartable - /** Inject into RingtonePlayer. */ + /** Inject into RingtonePlayer. */ @Binds @IntoMap @ClassKey(RingtonePlayer::class) @@ -179,50 +175,49 @@ abstract class SystemUICoreStartableModule { @ClassKey(GesturePointerEventListener::class) abstract fun bindGesturePointerEventListener(sysui: GesturePointerEventListener): CoreStartable - /** Inject into SessionTracker. */ + /** Inject into SessionTracker. */ @Binds @IntoMap @ClassKey(SessionTracker::class) abstract fun bindSessionTracker(service: SessionTracker): CoreStartable - /** Inject into ShortcutKeyDispatcher. */ + /** Inject into ShortcutKeyDispatcher. */ @Binds @IntoMap @ClassKey(ShortcutKeyDispatcher::class) abstract fun bindShortcutKeyDispatcher(sysui: ShortcutKeyDispatcher): CoreStartable - /** Inject into SliceBroadcastRelayHandler. */ + /** Inject into SliceBroadcastRelayHandler. */ @Binds @IntoMap @ClassKey(SliceBroadcastRelayHandler::class) abstract fun bindSliceBroadcastRelayHandler(sysui: SliceBroadcastRelayHandler): CoreStartable - /** Inject into StorageNotification. */ + /** Inject into StorageNotification. */ @Binds @IntoMap @ClassKey(StorageNotification::class) abstract fun bindStorageNotification(sysui: StorageNotification): CoreStartable - /** Inject into ThemeOverlayController. */ + /** Inject into ThemeOverlayController. */ @Binds @IntoMap @ClassKey(ThemeOverlayController::class) abstract fun bindThemeOverlayController(sysui: ThemeOverlayController): CoreStartable - - /** Inject into MediaOutputSwitcherDialogUI. */ + /** Inject into MediaOutputSwitcherDialogUI. */ @Binds @IntoMap @ClassKey(MediaOutputSwitcherDialogUI::class) abstract fun MediaOutputSwitcherDialogUI(sysui: MediaOutputSwitcherDialogUI): CoreStartable - /** Inject into Magnification. */ + /** Inject into Magnification. */ @Binds @IntoMap @ClassKey(Magnification::class) abstract fun bindMagnification(sysui: Magnification): CoreStartable - /** Inject into WMShell. */ + /** Inject into WMShell. */ @Binds @IntoMap @ClassKey(WMShell::class) @@ -239,7 +234,7 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(MediaTttChipControllerReceiver::class) abstract fun bindMediaTttChipControllerReceiver( - sysui: MediaTttChipControllerReceiver + sysui: MediaTttChipControllerReceiver ): CoreStartable /** Inject into MediaTttCommandLineHelper. */ @@ -254,8 +249,6 @@ abstract class SystemUICoreStartableModule { @ClassKey(ChipbarCoordinator::class) abstract fun bindChipbarController(sysui: ChipbarCoordinator): CoreStartable - - /** Inject into StylusUsiPowerStartable) */ @Binds @IntoMap @@ -267,21 +260,21 @@ abstract class SystemUICoreStartableModule { @ClassKey(PhysicalKeyboardCoreStartable::class) abstract fun bindKeyboardCoreStartable(listener: PhysicalKeyboardCoreStartable): CoreStartable - /** Inject into MuteQuickAffordanceCoreStartable*/ + /** Inject into MuteQuickAffordanceCoreStartable */ @Binds @IntoMap @ClassKey(MuteQuickAffordanceCoreStartable::class) abstract fun bindMuteQuickAffordanceCoreStartable( - sysui: MuteQuickAffordanceCoreStartable + sysui: MuteQuickAffordanceCoreStartable ): CoreStartable - /**Inject into DreamMonitor */ + /** Inject into DreamMonitor */ @Binds @IntoMap @ClassKey(DreamMonitor::class) abstract fun bindDreamMonitor(sysui: DreamMonitor): CoreStartable - /**Inject into AssistantAttentionMonitor */ + /** Inject into AssistantAttentionMonitor */ @Binds @IntoMap @ClassKey(AssistantAttentionMonitor::class) @@ -321,35 +314,6 @@ abstract class SystemUICoreStartableModule { @Binds @IntoMap - @ClassKey(CommunalLoggerStartable::class) - abstract fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable - - @Binds - @IntoMap - @ClassKey(CommunalSceneStartable::class) - abstract fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable - - @Binds - @IntoMap - @ClassKey(CommunalDreamStartable::class) - abstract fun bindCommunalDreamStartable(impl: CommunalDreamStartable): CoreStartable - - @Binds - @IntoMap - @ClassKey(CommunalAppWidgetHostStartable::class) - abstract fun bindCommunalAppWidgetHostStartable( - impl: CommunalAppWidgetHostStartable - ): CoreStartable - - @Binds - @IntoMap - @ClassKey(CommunalBackupRestoreStartable::class) - abstract fun bindCommunalBackupRestoreStartable( - impl: CommunalBackupRestoreStartable - ): CoreStartable - - @Binds - @IntoMap @ClassKey(HomeControlsDreamStartable::class) abstract fun bindHomeControlsDreamStartable(impl: HomeControlsDreamStartable): CoreStartable diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index c6c5747973b3..83fa001d104e 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -45,6 +45,7 @@ import androidx.lifecycle.LifecycleService; import androidx.lifecycle.ServiceLifecycleDispatcher; import androidx.lifecycle.ViewModelStore; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.dream.lowlight.dagger.LowLightDreamModule; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; @@ -97,7 +98,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ @Nullable private final ComponentName mHomeControlPanelDreamComponent; private final UiEventLogger mUiEventLogger; - private final WindowManager mWindowManager; + private final ViewCaptureAwareWindowManager mWindowManager; private final String mWindowTitle; // A reference to the {@link Window} used to hold the dream overlay. @@ -244,7 +245,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ Context context, DreamOverlayLifecycleOwner lifecycleOwner, @Main DelayableExecutor executor, - WindowManager windowManager, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, ComplicationComponent.Factory complicationComponentFactory, com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory dreamComplicationComponentFactory, @@ -267,7 +268,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ super(executor); mContext = context; mExecutor = executor; - mWindowManager = windowManager; + mWindowManager = viewCaptureAwareWindowManager; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mScrimManager = scrimManager; mLowLightDreamComponent = lowLightDreamComponent; diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt index 7b5139aa510e..c44eb471d460 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt @@ -17,8 +17,10 @@ package com.android.systemui.haptics.qs import android.os.VibrationEffect +import android.service.quicksettings.Tile import androidx.annotation.VisibleForTesting import com.android.systemui.animation.Expandable +import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController @@ -40,6 +42,7 @@ class QSLongPressEffect constructor( private val vibratorHelper: VibratorHelper?, private val keyguardStateController: KeyguardStateController, + private val falsingManager: FalsingManager, ) { var effectDuration = 0 @@ -130,18 +133,18 @@ constructor( fun handleAnimationComplete() { when (state) { State.RUNNING_FORWARD -> { - setState(State.IDLE) vibrate(snapEffect) if (keyguardStateController.isUnlocked) { - qsTile?.longClick(expandable) + setState(State.LONG_CLICKED) } else { callback?.onResetProperties() - qsTile?.longClick(expandable) + setState(State.IDLE) } + qsTile?.longClick(expandable) } State.RUNNING_BACKWARDS_FROM_UP -> { - setState(State.IDLE) callback?.onEffectFinishedReversing() + setState(getStateForClick()) qsTile?.click(expandable) } State.RUNNING_BACKWARDS_FROM_CANCEL -> setState(State.IDLE) @@ -160,14 +163,37 @@ constructor( } fun onTileClick(): Boolean { - if (state == State.TIMEOUT_WAIT) { - setState(State.IDLE) - qsTile?.let { - it.click(expandable) - return true - } + val isStateClickable = state == State.TIMEOUT_WAIT || state == State.IDLE + + // Ignore View-generated clicks on invalid states or if the bouncer is showing + if (keyguardStateController.isPrimaryBouncerShowing || !isStateClickable) return false + + setState(getStateForClick()) + qsTile?.click(expandable) + return true + } + + /** + * Get the appropriate state for a click action. + * + * In some occasions, the click action will not result in a subsequent action that resets the + * state upon completion (e.g., a launch transition animation). In these cases, the state needs + * to be reset before the click is dispatched. + */ + @VisibleForTesting + fun getStateForClick(): State { + val isTileUnavailable = qsTile?.state?.state == Tile.STATE_UNAVAILABLE + val isFalseTapWhileLocked = + !keyguardStateController.isUnlocked && + falsingManager.isFalseTap(FalsingManager.LOW_PENALTY) + val handlesLongClick = qsTile?.state?.handlesLongClick == true + return if (isTileUnavailable || isFalseTapWhileLocked || !handlesLongClick) { + // The click event will not perform an action that resets the state. Therefore, this is + // the last opportunity to reset the state back to IDLE. + State.IDLE + } else { + State.CLICKED } - return false } /** @@ -194,6 +220,8 @@ constructor( return true } + fun resetState() = setState(State.IDLE) + enum class State { IDLE, /* The effect is idle waiting for touch input */ TIMEOUT_WAIT, /* The effect is waiting for a tap timeout period */ @@ -202,6 +230,8 @@ constructor( RUNNING_BACKWARDS_FROM_UP, /* The effect was interrupted by an ACTION_CANCEL and is now running backwards */ RUNNING_BACKWARDS_FROM_CANCEL, + CLICKED, /* The effect has ended with a click */ + LONG_CLICKED, /* The effect has ended with a long-click */ } /** Callbacks to notify view and animator actions */ diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt index 1f0aef8ab977..906f600e3826 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt @@ -21,11 +21,13 @@ import com.android.systemui.CoreStartable import com.android.systemui.Flags.keyboardShortcutHelperRewrite import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource +import com.android.systemui.keyboard.shortcut.data.source.CurrentAppShortcutsSource import com.android.systemui.keyboard.shortcut.data.source.InputShortcutsSource import com.android.systemui.keyboard.shortcut.data.source.KeyboardShortcutGroupsSource import com.android.systemui.keyboard.shortcut.data.source.MultitaskingShortcutsSource import com.android.systemui.keyboard.shortcut.data.source.SystemShortcutsSource import com.android.systemui.keyboard.shortcut.qualifiers.AppCategoriesShortcuts +import com.android.systemui.keyboard.shortcut.qualifiers.CurrentAppShortcuts import com.android.systemui.keyboard.shortcut.qualifiers.InputShortcuts import com.android.systemui.keyboard.shortcut.qualifiers.MultitaskingShortcuts import com.android.systemui.keyboard.shortcut.qualifiers.SystemShortcuts @@ -55,6 +57,10 @@ interface ShortcutHelperModule { fun multitaskingShortcutsSource(impl: MultitaskingShortcutsSource): KeyboardShortcutGroupsSource @Binds + @CurrentAppShortcuts + fun currentAppShortcutsSource(impl: CurrentAppShortcutsSource): KeyboardShortcutGroupsSource + + @Binds @InputShortcuts fun inputShortcutsSources(impl: InputShortcutsSource): KeyboardShortcutGroupsSource diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt index 7b0c25e8379d..49817b263583 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt @@ -20,6 +20,7 @@ import android.content.Context import android.graphics.drawable.Icon import android.hardware.input.InputManager import android.util.Log +import android.view.InputDevice import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.KeyboardShortcutGroup @@ -28,16 +29,18 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyboard.shortcut.data.source.KeyboardShortcutGroupsSource import com.android.systemui.keyboard.shortcut.qualifiers.AppCategoriesShortcuts +import com.android.systemui.keyboard.shortcut.qualifiers.CurrentAppShortcuts import com.android.systemui.keyboard.shortcut.qualifiers.InputShortcuts import com.android.systemui.keyboard.shortcut.qualifiers.MultitaskingShortcuts import com.android.systemui.keyboard.shortcut.qualifiers.SystemShortcuts import com.android.systemui.keyboard.shortcut.shared.model.Shortcut import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.APP_CATEGORIES -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.IME -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MULTI_TASKING -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.SYSTEM +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.CurrentApp +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon @@ -45,7 +48,11 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @SysUISingleton @@ -53,11 +60,13 @@ class ShortcutHelperCategoriesRepository @Inject constructor( private val context: Context, + @Background private val backgroundScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, @SystemShortcuts private val systemShortcutsSource: KeyboardShortcutGroupsSource, @MultitaskingShortcuts private val multitaskingShortcutsSource: KeyboardShortcutGroupsSource, @AppCategoriesShortcuts private val appCategoriesShortcutsSource: KeyboardShortcutGroupsSource, @InputShortcuts private val inputShortcutsSource: KeyboardShortcutGroupsSource, + @CurrentAppShortcuts private val currentAppShortcutsSource: KeyboardShortcutGroupsSource, private val inputManager: InputManager, stateRepository: ShortcutHelperStateRepository ) { @@ -71,61 +80,82 @@ constructor( } } - val systemShortcutsCategory = - activeInputDevice.map { - if (it != null) { - toShortcutCategory( - it.keyCharacterMap, - SYSTEM, - systemShortcutsSource.shortcutGroups(it.id), - keepIcons = true, + val categories: Flow<List<ShortcutCategory>> = + activeInputDevice + .map { + if (it == null) { + return@map emptyList() + } + return@map listOfNotNull( + fetchSystemShortcuts(it), + fetchMultiTaskingShortcuts(it), + fetchAppCategoriesShortcuts(it), + fetchImeShortcuts(it), + fetchCurrentAppShortcuts(it), ) - } else { - null } - } + .stateIn( + scope = backgroundScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) - val multitaskingShortcutsCategory = - activeInputDevice.map { - if (it != null) { - toShortcutCategory( - it.keyCharacterMap, - MULTI_TASKING, - multitaskingShortcutsSource.shortcutGroups(it.id), - keepIcons = true, - ) - } else { - null - } - } + private suspend fun fetchSystemShortcuts(inputDevice: InputDevice) = + toShortcutCategory( + inputDevice.keyCharacterMap, + System, + systemShortcutsSource.shortcutGroups(inputDevice.id), + keepIcons = true, + ) - val appCategoriesShortcutsCategory = - activeInputDevice.map { - if (it != null) { - toShortcutCategory( - it.keyCharacterMap, - APP_CATEGORIES, - appCategoriesShortcutsSource.shortcutGroups(it.id), - keepIcons = true, - ) - } else { - null - } + private suspend fun fetchMultiTaskingShortcuts(inputDevice: InputDevice) = + toShortcutCategory( + inputDevice.keyCharacterMap, + MultiTasking, + multitaskingShortcutsSource.shortcutGroups(inputDevice.id), + keepIcons = true, + ) + + private suspend fun fetchAppCategoriesShortcuts(inputDevice: InputDevice) = + toShortcutCategory( + inputDevice.keyCharacterMap, + AppCategories, + appCategoriesShortcutsSource.shortcutGroups(inputDevice.id), + keepIcons = true, + ) + + private suspend fun fetchImeShortcuts(inputDevice: InputDevice) = + toShortcutCategory( + inputDevice.keyCharacterMap, + InputMethodEditor, + inputShortcutsSource.shortcutGroups(inputDevice.id), + keepIcons = false, + ) + + private suspend fun fetchCurrentAppShortcuts(inputDevice: InputDevice): ShortcutCategory? { + val shortcutGroups = currentAppShortcutsSource.shortcutGroups(inputDevice.id) + val categoryType = getCurrentAppShortcutCategoryType(shortcutGroups) + return if (categoryType == null) { + null + } else { + toShortcutCategory( + inputDevice.keyCharacterMap, + categoryType, + shortcutGroups, + keepIcons = false + ) } + } - val imeShortcutsCategory = - activeInputDevice.map { - if (it != null) { - toShortcutCategory( - it.keyCharacterMap, - IME, - inputShortcutsSource.shortcutGroups(it.id), - keepIcons = false, - ) - } else { - null - } + private fun getCurrentAppShortcutCategoryType( + shortcutGroups: List<KeyboardShortcutGroup> + ): ShortcutCategoryType? { + return if (shortcutGroups.isEmpty()) { + null + } else { + CurrentApp(packageName = shortcutGroups[0].packageName.toString()) } + } private fun toShortcutCategory( keyCharacterMap: KeyCharacterMap, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSource.kt new file mode 100644 index 000000000000..7e6ed19d6070 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSource.kt @@ -0,0 +1,34 @@ +/* + * 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.keyboard.shortcut.data.source + +import android.view.KeyboardShortcutGroup +import android.view.WindowManager +import android.view.WindowManager.KeyboardShortcutsReceiver +import javax.inject.Inject +import kotlinx.coroutines.suspendCancellableCoroutine + +class CurrentAppShortcutsSource @Inject constructor(private val windowManager: WindowManager) : + KeyboardShortcutGroupsSource { + override suspend fun shortcutGroups(deviceId: Int): List<KeyboardShortcutGroup> = + suspendCancellableCoroutine { continuation -> + val shortcutsReceiver = KeyboardShortcutsReceiver { + continuation.resumeWith(Result.success(it ?: emptyList())) + } + windowManager.requestAppKeyboardShortcuts(shortcutsReceiver, deviceId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSource.kt index aba441546e35..1b2098650278 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSource.kt @@ -59,7 +59,7 @@ constructor(@Main private val resources: Resources, private val windowManager: W private suspend fun getImeShortcutGroup(deviceId: Int): List<KeyboardShortcutGroup> = suspendCancellableCoroutine { continuation -> val shortcutsReceiver = KeyboardShortcutsReceiver { - continuation.resumeWith(Result.success(it)) + continuation.resumeWith(Result.success(it ?: emptyList())) } windowManager.requestImeKeyboardShortcuts(shortcutsReceiver, deviceId) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt index d41d21a3b4fb..6f19561dd87b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt @@ -23,7 +23,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map @SysUISingleton class ShortcutHelperCategoriesInteractor @@ -33,13 +33,8 @@ constructor( ) { val shortcutCategories: Flow<List<ShortcutCategory>> = - combine( - categoriesRepository.systemShortcutsCategory, - categoriesRepository.multitaskingShortcutsCategory, - categoriesRepository.imeShortcutsCategory, - categoriesRepository.appCategoriesShortcutsCategory, - ) { shortcutCategories -> - shortcutCategories.filterNotNull().map { groupSubCategoriesInCategory(it) } + categoriesRepository.categories.map { categories -> + categories.map { category -> groupSubCategoriesInCategory(category) } } private fun groupSubCategoriesInCategory(shortcutCategory: ShortcutCategory): ShortcutCategory { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/CurrentAppShortcuts.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/CurrentAppShortcuts.kt new file mode 100644 index 000000000000..51631b11b27c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/CurrentAppShortcuts.kt @@ -0,0 +1,21 @@ +/* + * 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.keyboard.shortcut.qualifiers + +import javax.inject.Qualifier + +@Qualifier annotation class CurrentAppShortcuts diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt index 63e167a376d1..4eabefcd2d41 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt @@ -16,11 +16,16 @@ package com.android.systemui.keyboard.shortcut.shared.model -enum class ShortcutCategoryType { - SYSTEM, - MULTI_TASKING, - IME, - APP_CATEGORIES, +sealed interface ShortcutCategoryType { + data object System : ShortcutCategoryType + + data object MultiTasking : ShortcutCategoryType + + data object InputMethodEditor : ShortcutCategoryType + + data object AppCategories : ShortcutCategoryType + + data class CurrentApp(val packageName: String) : ShortcutCategoryType } data class ShortcutCategory( diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt index 3b037bc17564..9e9368d3ffd3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -16,7 +16,10 @@ package com.android.systemui.keyboard.shortcut.ui.composable +import android.content.Context +import android.content.pm.PackageManager.NameNotFoundException import android.graphics.drawable.Icon +import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Image @@ -55,6 +58,7 @@ import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationDrawerItemColors import androidx.compose.material3.NavigationDrawerItemDefaults @@ -76,7 +80,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.rememberNestedScrollInteropConnection @@ -99,8 +102,10 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory +import com.android.systemui.keyboard.shortcut.ui.model.IconSource import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.CentralSurfaces @Composable fun ShortcutHelper( @@ -210,9 +215,9 @@ private fun CategoryItemSinglePane( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp) ) { - Icon(category.icon, contentDescription = null) + ShortcutCategoryIcon(category.icon) Spacer(modifier = Modifier.width(16.dp)) - Text(stringResource(category.labelResId)) + Text(category.label(LocalContext.current)) Spacer(modifier = Modifier.weight(1f)) RotatingExpandCollapseIcon(isExpanded) } @@ -221,23 +226,67 @@ private fun CategoryItemSinglePane( } } -private val ShortcutCategory.icon: ImageVector +private val ShortcutCategory.icon: IconSource + @Composable get() = when (type) { - ShortcutCategoryType.SYSTEM -> Icons.Default.Tv - ShortcutCategoryType.MULTI_TASKING -> Icons.Default.VerticalSplit - ShortcutCategoryType.IME -> Icons.Default.Keyboard - ShortcutCategoryType.APP_CATEGORIES -> Icons.Default.Apps + ShortcutCategoryType.System -> IconSource(imageVector = Icons.Default.Tv) + ShortcutCategoryType.MultiTasking -> + IconSource(imageVector = Icons.Default.VerticalSplit) + ShortcutCategoryType.InputMethodEditor -> + IconSource(imageVector = Icons.Default.Keyboard) + ShortcutCategoryType.AppCategories -> IconSource(imageVector = Icons.Default.Apps) + is ShortcutCategoryType.CurrentApp -> { + val context = LocalContext.current + val iconDrawable = context.packageManager.getApplicationIcon(type.packageName) + IconSource(painter = rememberDrawablePainter(drawable = iconDrawable)) + } } -private val ShortcutCategory.labelResId: Int - get() = - when (type) { - ShortcutCategoryType.SYSTEM -> R.string.shortcut_helper_category_system - ShortcutCategoryType.MULTI_TASKING -> R.string.shortcut_helper_category_multitasking - ShortcutCategoryType.IME -> R.string.shortcut_helper_category_input - ShortcutCategoryType.APP_CATEGORIES -> R.string.shortcut_helper_category_app_shortcuts - } +@Composable +fun ShortcutCategoryIcon( + source: IconSource, + contentDescription: String? = null, + modifier: Modifier = Modifier, + tint: Color = LocalContentColor.current +) { + if (source.imageVector != null) { + Icon(source.imageVector, contentDescription, modifier, tint) + } else if (source.painter != null) { + Image(source.painter, contentDescription, modifier) + } +} + +private fun ShortcutCategory.label(context: Context): String = + when (type) { + ShortcutCategoryType.System -> context.getString(R.string.shortcut_helper_category_system) + ShortcutCategoryType.MultiTasking -> + context.getString(R.string.shortcut_helper_category_multitasking) + ShortcutCategoryType.InputMethodEditor -> + context.getString(R.string.shortcut_helper_category_input) + ShortcutCategoryType.AppCategories -> + context.getString(R.string.shortcut_helper_category_app_shortcuts) + is ShortcutCategoryType.CurrentApp -> getApplicationLabelForCurrentApp(type, context) + } + +private fun getApplicationLabelForCurrentApp( + type: ShortcutCategoryType.CurrentApp, + context: Context +): String { + val packageManagerForUser = CentralSurfaces.getPackageManagerForUser(context, context.userId) + return try { + val currentAppInfo = + packageManagerForUser.getApplicationInfoAsUser( + type.packageName, + /* flags = */ 0, + context.userId + ) + packageManagerForUser.getApplicationLabel(currentAppInfo).toString() + } catch (e: NameNotFoundException) { + Log.wtf(ShortcutHelper.TAG, "Couldn't find app info by package name ${type.packageName}") + context.getString(R.string.shortcut_helper_category_current_app_shortcuts) + } +} @Composable private fun RotatingExpandCollapseIcon(isExpanded: Boolean) { @@ -525,8 +574,8 @@ private fun CategoriesPanelTwoPane( Column { categories.fastForEach { CategoryItemTwoPane( - label = stringResource(it.labelResId), - icon = it.icon, + label = it.label(LocalContext.current), + iconSource = it.icon, selected = selectedCategory == it.type, onClick = { onCategoryClicked(it) } ) @@ -537,7 +586,7 @@ private fun CategoriesPanelTwoPane( @Composable private fun CategoryItemTwoPane( label: String, - icon: ImageVector, + iconSource: IconSource, selected: Boolean, onClick: () -> Unit, colors: NavigationDrawerItemColors = @@ -551,9 +600,9 @@ private fun CategoryItemTwoPane( color = colors.containerColor(selected).value, ) { Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) { - Icon( + ShortcutCategoryIcon( modifier = Modifier.size(24.dp), - imageVector = icon, + source = iconSource, contentDescription = null, tint = colors.iconColor(selected).value ) @@ -649,4 +698,6 @@ object ShortcutHelper { object Dimensions { val SinglePaneCategoryCornerRadius = 28.dp } + + internal const val TAG = "ShortcutHelperUI" } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/IconSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/IconSource.kt new file mode 100644 index 000000000000..7fc0103d861f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/IconSource.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.ui.model + +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector + +data class IconSource(val imageVector: ImageVector? = null, val painter: Painter? = null) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt index e602cad30daa..ad258f435d63 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyboard.shortcut.ui.viewmodel import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -40,8 +39,8 @@ constructor( ) { val shouldShow = - stateInteractor.state - .map { it is ShortcutHelperState.Active } + categoriesInteractor.shortcutCategories + .map { it.isNotEmpty() } .distinctUntilChanged() .flowOn(backgroundDispatcher) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 33f9209fea09..80cf4c50866c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -21,6 +21,7 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.constraintlayout.widget.ConstraintSet @@ -29,9 +30,9 @@ import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayout -import com.android.compose.animation.scene.transitions import com.android.internal.jank.InteractionJankMonitor import com.android.keyguard.KeyguardStatusView import com.android.keyguard.KeyguardStatusViewController @@ -115,7 +116,6 @@ constructor( private var rootViewHandle: DisposableHandle? = null private var indicationAreaHandle: DisposableHandle? = null - private val sceneKey = SceneKey("root-view-scene-key") var keyguardStatusViewController: KeyguardStatusViewController? = null get() { @@ -233,12 +233,10 @@ constructor( setContent { // STL is used solely to provide a SceneScope to enable us to invoke SceneScope // composables. - SceneTransitionLayout( - currentScene = sceneKey, - onChangeScene = {}, - transitions = transitions {}, - ) { - scene(sceneKey) { + val currentScene = remember { SceneKey("root-view-scene-key") } + val state = remember { MutableSceneTransitionLayoutState(currentScene) } + SceneTransitionLayout(state) { + scene(currentScene) { with( LockscreenContent( viewModel = viewModel, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index aee65a81880d..cd28bec938b8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -17,8 +17,11 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.app.DreamManager import com.android.app.animation.Interpolators import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -28,6 +31,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.util.kotlin.Utils.Companion.sample import com.android.systemui.util.kotlin.sample @@ -53,9 +57,11 @@ constructor( keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, + private val communalSceneInteractor: CommunalSceneInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, val deviceEntryRepository: DeviceEntryRepository, private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor, + private val dreamManager: DreamManager, ) : TransitionInteractor( fromState = KeyguardState.DOZING, @@ -115,6 +121,7 @@ constructor( } } + @SuppressLint("MissingPermission") private fun listenForDozingToAny() { if (KeyguardWmStateRefactor.isEnabled) { return @@ -126,7 +133,8 @@ constructor( .filterRelevantKeyguardStateAnd { isAwake -> isAwake } .sample( keyguardInteractor.isKeyguardOccluded, - communalInteractor.isIdleOnCommunal, + communalInteractor.isCommunalAvailable, + communalSceneInteractor.isIdleOnCommunal, canTransitionToGoneOnWake, keyguardInteractor.primaryBouncerShowing, ) @@ -134,6 +142,7 @@ constructor( ( _, occluded, + isCommunalAvailable, isIdleOnCommunal, canTransitionToGoneOnWake, primaryBouncerShowing) -> @@ -163,6 +172,19 @@ constructor( } else { startTransitionTo(KeyguardState.GLANCEABLE_HUB) } + } else if ( + powerInteractor.detailedWakefulness.value.lastWakeReason == + WakeSleepReason.POWER_BUTTON && + isCommunalAvailable && + dreamManager.canStartDreaming(true) + ) { + // This case handles tapping the power button to transition through + // dream -> off -> hub. + if (SceneContainerFlag.isEnabled) { + // TODO(b/336576536): Check if adaptation for scene framework is needed + } else { + startTransitionTo(KeyguardState.GLANCEABLE_HUB) + } } else { startTransitionTo(KeyguardState.LOCKSCREEN) } @@ -171,6 +193,7 @@ constructor( } /** Figure out what state to transition to when we awake from DOZING. */ + @SuppressLint("MissingPermission") private fun listenForWakeFromDozing() { if (!KeyguardWmStateRefactor.isEnabled) { return @@ -180,7 +203,8 @@ constructor( powerInteractor.detailedWakefulness .filterRelevantKeyguardStateAnd { it.isAwake() } .sample( - communalInteractor.isIdleOnCommunal, + communalInteractor.isCommunalAvailable, + communalSceneInteractor.isIdleOnCommunal, keyguardInteractor.biometricUnlockState, wakeToGoneInteractor.canWakeDirectlyToGone, keyguardInteractor.primaryBouncerShowing, @@ -188,6 +212,7 @@ constructor( .collect { ( _, + isCommunalAvailable, isIdleOnCommunal, biometricUnlockState, canWakeDirectlyToGone, @@ -227,6 +252,23 @@ constructor( ownerReason = "waking from dozing" ) } + } else if ( + powerInteractor.detailedWakefulness.value.lastWakeReason == + WakeSleepReason.POWER_BUTTON && + isCommunalAvailable && + dreamManager.canStartDreaming(true) + ) { + // This case handles tapping the power button to transition through + // dream -> off -> hub. + if (SceneContainerFlag.isEnabled) { + // TODO(b/336576536): Check if adaptation for scene framework is + // needed + } else { + startTransitionTo( + KeyguardState.GLANCEABLE_HUB, + ownerReason = "waking from dozing" + ) + } } else { startTransitionTo( KeyguardState.LOCKSCREEN, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt index 41c39597dc02..e2bb540f6645 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.domain.interactor -import android.content.Context import com.android.systemui.CoreStartable import com.android.systemui.biometrics.domain.interactor.FingerprintPropertyInteractor import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor @@ -32,15 +31,12 @@ import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBl import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type -import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -51,13 +47,10 @@ class KeyguardBlueprintInteractor constructor( private val keyguardBlueprintRepository: KeyguardBlueprintRepository, @Application private val applicationScope: CoroutineScope, - private val context: Context, - private val shadeInteractor: ShadeInteractor, - private val clockInteractor: KeyguardClockInteractor, + shadeInteractor: ShadeInteractor, private val configurationInteractor: ConfigurationInteractor, private val fingerprintPropertyInteractor: FingerprintPropertyInteractor, private val smartspaceSection: SmartspaceSection, - private val clockSection: ClockSection, ) : CoreStartable { /** The current blueprint for the lockscreen. */ val blueprint: StateFlow<KeyguardBlueprint> = keyguardBlueprintRepository.blueprint @@ -70,8 +63,8 @@ constructor( /** Current BlueprintId */ val blueprintId = - shadeInteractor.shadeMode.map { shadeMode -> - val useSplitShade = shadeMode == ShadeMode.Split && !ComposeLockscreen.isEnabled + shadeInteractor.isShadeLayoutWide.map { isShadeLayoutWide -> + val useSplitShade = isShadeLayoutWide && !ComposeLockscreen.isEnabled when { useSplitShade -> SplitShadeKeyguardBlueprint.ID else -> DefaultKeyguardBlueprint.DEFAULT diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt index 142b1a031277..c0049d4e2e6c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt @@ -31,7 +31,6 @@ import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockId import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.util.kotlin.combine @@ -45,6 +44,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn private val TAG = KeyguardClockInteractor::class.simpleName + /** Manages and encapsulates the clock components of the lockscreen root view. */ @SysUISingleton class KeyguardClockInteractor @@ -77,16 +77,16 @@ constructor( val clockSize: StateFlow<ClockSize> = if (SceneContainerFlag.isEnabled) { combine( - shadeInteractor.shadeMode, + shadeInteractor.isShadeLayoutWide, activeNotificationsInteractor.areAnyNotificationsPresent, mediaCarouselInteractor.hasActiveMediaOrRecommendation, keyguardInteractor.isDozing, isOnAod, - ) { shadeMode, hasNotifs, hasMedia, isDozing, isOnAod -> + ) { isShadeLayoutWide, hasNotifs, hasMedia, isDozing, isOnAod -> return@combine when { keyguardClockRepository.shouldForceSmallClock && !isOnAod -> ClockSize.SMALL - shadeMode == ShadeMode.Single && (hasNotifs || hasMedia) -> ClockSize.SMALL - shadeMode == ShadeMode.Single -> ClockSize.LARGE + !isShadeLayoutWide && (hasNotifs || hasMedia) -> ClockSize.SMALL + !isShadeLayoutWide -> ClockSize.LARGE hasMedia && !isDozing -> ClockSize.SMALL else -> ClockSize.LARGE } @@ -103,21 +103,21 @@ constructor( val clockShouldBeCentered: Flow<Boolean> = if (SceneContainerFlag.isEnabled) { combine( - shadeInteractor.shadeMode, + shadeInteractor.isShadeLayoutWide, activeNotificationsInteractor.areAnyNotificationsPresent, keyguardInteractor.isActiveDreamLockscreenHosted, isOnAod, headsUpNotificationInteractor.isHeadsUpOrAnimatingAway, keyguardInteractor.isDozing, ) { - shadeMode, + isShadeLayoutWide, areAnyNotificationsPresent, isActiveDreamLockscreenHosted, isOnAod, isHeadsUp, isDozing -> when { - shadeMode != ShadeMode.Split -> true + !isShadeLayoutWide -> true !areAnyNotificationsPresent -> true isActiveDreamLockscreenHosted -> true // Pulsing notification appears on the right. Move clock left to avoid overlap. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt index 6a1b7cfb7d7e..f0bf4029ab39 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt @@ -29,6 +29,7 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.KeyguardState @@ -257,6 +258,10 @@ constructor( /** Set an alarm for */ private fun setResetCanIgnoreAuthAlarm() { + if (!KeyguardWmStateRefactor.isEnabled) { + return + } + val intent = Intent(DELAYED_KEYGUARD_ACTION).apply { setPackage(context.packageName) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index 573b75e623fb..73028c5cf496 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -30,7 +30,6 @@ import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel import com.android.systemui.statusbar.ui.SystemBarUtilsProxy import javax.inject.Inject @@ -119,26 +118,25 @@ constructor( combine( isLargeClockVisible, clockShouldBeCentered, - shadeInteractor.shadeMode, + shadeInteractor.isShadeLayoutWide, currentClock, - ) { isLargeClockVisible, clockShouldBeCentered, shadeMode, currentClock -> - val shouldUseSplitShade = shadeMode == ShadeMode.Split + ) { isLargeClockVisible, clockShouldBeCentered, isShadeLayoutWide, currentClock -> if (currentClock?.config?.useCustomClockScene == true) { when { - shouldUseSplitShade && clockShouldBeCentered -> + isShadeLayoutWide && clockShouldBeCentered -> ClockLayout.WEATHER_LARGE_CLOCK - shouldUseSplitShade && isLargeClockVisible -> + isShadeLayoutWide && isLargeClockVisible -> ClockLayout.SPLIT_SHADE_WEATHER_LARGE_CLOCK - shouldUseSplitShade -> ClockLayout.SPLIT_SHADE_SMALL_CLOCK + isShadeLayoutWide -> ClockLayout.SPLIT_SHADE_SMALL_CLOCK isLargeClockVisible -> ClockLayout.WEATHER_LARGE_CLOCK else -> ClockLayout.SMALL_CLOCK } } else { when { - shouldUseSplitShade && clockShouldBeCentered -> ClockLayout.LARGE_CLOCK - shouldUseSplitShade && isLargeClockVisible -> + isShadeLayoutWide && clockShouldBeCentered -> ClockLayout.LARGE_CLOCK + isShadeLayoutWide && isLargeClockVisible -> ClockLayout.SPLIT_SHADE_LARGE_CLOCK - shouldUseSplitShade -> ClockLayout.SPLIT_SHADE_SMALL_CLOCK + isShadeLayoutWide -> ClockLayout.SPLIT_SHADE_SMALL_CLOCK isLargeClockVisible -> ClockLayout.LARGE_CLOCK else -> ClockLayout.SMALL_CLOCK } @@ -164,7 +162,7 @@ constructor( /** Calculates the top margin for the small clock. */ fun getSmallClockTopMargin(): Int { val statusBarHeight = systemBarUtils.getStatusBarHeaderHeightKeyguard() - return if (shadeInteractor.shadeMode.value == ShadeMode.Split) { + return if (shadeInteractor.isShadeLayoutWide.value) { resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin) - if (ComposeLockscreen.isEnabled) statusBarHeight else 0 } else { @@ -176,7 +174,7 @@ constructor( val smallClockTopMargin = combine( configurationInteractor.onAnyConfigurationChange, - shadeInteractor.shadeMode, + shadeInteractor.isShadeLayoutWide, ) { _, _ -> getSmallClockTopMargin() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index 8a29f96d3fcd..3b337fc3f7c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -27,7 +27,6 @@ import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -56,21 +55,14 @@ constructor( val isUdfpsVisible: Boolean get() = authController.isUdfpsSupported - val shouldUseSplitNotificationShade: StateFlow<Boolean> = - shadeInteractor.shadeMode - .map { it == ShadeMode.Split } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = false, - ) + val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide val areNotificationsVisible: StateFlow<Boolean> = combine( clockSize, - shouldUseSplitNotificationShade, - ) { clockSize, shouldUseSplitNotificationShade -> - clockSize == ClockSize.SMALL || shouldUseSplitNotificationShade + shadeInteractor.isShadeLayoutWide, + ) { clockSize, isShadeLayoutWide -> + clockSize == ClockSize.SMALL || isShadeLayoutWide } .stateIn( scope = applicationScope, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index 48970f5976e5..46c5c188344e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -845,8 +845,25 @@ constructor( commonViewModels.addAll(viewModels) // Ensure we only show the needed UMOs in media carousel. - val viewSet = viewModels.toHashSet() - controllerByViewModel.filter { !viewSet.contains(it.key) }.forEach { onRemoved(it.key) } + val viewIds = + viewModels + .map { mediaCommonViewModel -> + when (mediaCommonViewModel) { + is MediaCommonViewModel.MediaControl -> + mediaCommonViewModel.instanceId.toString() + is MediaCommonViewModel.MediaRecommendations -> mediaCommonViewModel.key + } + } + .toHashSet() + controllerByViewModel + .filter { + when (val viewModel = it.key) { + is MediaCommonViewModel.MediaControl -> + !viewIds.contains(viewModel.instanceId.toString()) + is MediaCommonViewModel.MediaRecommendations -> !viewIds.contains(viewModel.key) + } + } + .forEach { onRemoved(it.key) } } private suspend fun getMediaLockScreenSetting(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt index de300b2ff900..82b48251cc8a 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt @@ -28,13 +28,25 @@ sealed interface MediaProjectionState { * * @property hostPackage the package name of the app that is receiving the content of the media * projection (aka which app the phone screen contents are being sent to). + * @property hostDeviceName the name of the other device that's receiving the content of the + * media projection. Null if the media projection is going to this same device (e.g. another + * app is recording the screen). */ - sealed class Projecting(open val hostPackage: String) : MediaProjectionState { + sealed class Projecting( + open val hostPackage: String, + open val hostDeviceName: String?, + ) : MediaProjectionState { /** The entire screen is being projected. */ - data class EntireScreen(override val hostPackage: String) : Projecting(hostPackage) + data class EntireScreen( + override val hostPackage: String, + override val hostDeviceName: String? = null, + ) : Projecting(hostPackage, hostDeviceName) /** Only a single task is being projected. */ - data class SingleTask(override val hostPackage: String, val task: RunningTaskInfo) : - Projecting(hostPackage) + data class SingleTask( + override val hostPackage: String, + override val hostDeviceName: String?, + val task: RunningTaskInfo, + ) : Projecting(hostPackage, hostDeviceName) } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt index 8a9adc7a5c88..760ff7dd1ed8 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.mediaprojection.data.repository import android.app.ActivityManager.RunningTaskInfo +import android.hardware.display.DisplayManager import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager import android.os.Handler @@ -35,18 +36,21 @@ import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @SysUISingleton +@OptIn(ExperimentalCoroutinesApi::class) class MediaProjectionManagerRepository @Inject constructor( private val mediaProjectionManager: MediaProjectionManager, + private val displayManager: DisplayManager, @Main private val handler: Handler, @Application private val applicationScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, @@ -74,12 +78,12 @@ constructor( object : MediaProjectionManager.Callback() { override fun onStart(info: MediaProjectionInfo?) { Log.d(TAG, "MediaProjectionManager.Callback#onStart") - trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG) + trySendWithFailureLogging(CallbackEvent.OnStart, TAG) } override fun onStop(info: MediaProjectionInfo?) { Log.d(TAG, "MediaProjectionManager.Callback#onStop") - trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG) + trySendWithFailureLogging(CallbackEvent.OnStop, TAG) } override fun onRecordingSessionSet( @@ -87,14 +91,36 @@ constructor( session: ContentRecordingSession? ) { Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session") - launch { - trySendWithFailureLogging(stateForSession(info, session), TAG) - } + trySendWithFailureLogging( + CallbackEvent.OnRecordingSessionSet(info, session), + TAG, + ) } } mediaProjectionManager.addCallback(callback, handler) awaitClose { mediaProjectionManager.removeCallback(callback) } } + // When we get an #onRecordingSessionSet event, we need to do some work in the + // background before emitting the right state value. But when we get an #onStop + // event, we immediately know what state value to emit. + // + // Without `mapLatest`, this could be a problem if an #onRecordingSessionSet event + // comes in and then an #onStop event comes in shortly afterwards (b/352483752): + // 1. #onRecordingSessionSet -> start some work in the background + // 2. #onStop -> immediately emit "Not Projecting" + // 3. onRecordingSessionSet work finishes -> emit "Projecting" + // + // At step 3, we *shouldn't* emit "Projecting" because #onStop was the last callback + // event we received, so we should be "Not Projecting". This `mapLatest` ensures + // that if an #onStop event comes in, we cancel any ongoing work for + // #onRecordingSessionSet and we don't emit "Projecting". + .mapLatest { + when (it) { + is CallbackEvent.OnStart, + is CallbackEvent.OnStop -> MediaProjectionState.NotProjecting + is CallbackEvent.OnRecordingSessionSet -> stateForSession(it.info, it.session) + } + } .stateIn( scope = applicationScope, started = SharingStarted.Lazily, @@ -110,14 +136,36 @@ constructor( } val hostPackage = info.packageName + val hostDeviceName = + withContext(backgroundDispatcher) { + // If the projection is to a different device, then the session's display ID should + // identify the display associated with that different device. + displayManager.getDisplay(session.virtualDisplayId)?.name + } + if (session.contentToRecord == RECORD_CONTENT_DISPLAY || session.tokenToRecord == null) { - return MediaProjectionState.Projecting.EntireScreen(hostPackage) + return MediaProjectionState.Projecting.EntireScreen(hostPackage, hostDeviceName) } val matchingTask = tasksRepository.findRunningTaskFromWindowContainerToken( checkNotNull(session.tokenToRecord) - ) ?: return MediaProjectionState.Projecting.EntireScreen(hostPackage) - return MediaProjectionState.Projecting.SingleTask(hostPackage, matchingTask) + ) ?: return MediaProjectionState.Projecting.EntireScreen(hostPackage, hostDeviceName) + return MediaProjectionState.Projecting.SingleTask(hostPackage, hostDeviceName, matchingTask) + } + + /** + * Translates [MediaProjectionManager.Callback] events into objects so that we always maintain + * the correct callback ordering. + */ + sealed interface CallbackEvent { + data object OnStart : CallbackEvent + + data object OnStop : CallbackEvent + + data class OnRecordingSessionSet( + val info: MediaProjectionInfo, + val session: ContentRecordingSession?, + ) : CallbackEvent } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java index d8c96dd182b4..eadbffe49495 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java @@ -41,6 +41,8 @@ import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.people.PeopleBackupFollowUpJob; import com.android.systemui.people.SharedPreferencesHelper; @@ -67,6 +69,7 @@ public class PeopleBackupHelper extends SharedPreferencesBackupHelper { private final UserHandle mUserHandle; private final PackageManager mPackageManager; private final IPeopleManager mIPeopleManager; + @Nullable private final AppWidgetManager mAppWidgetManager; /** @@ -404,6 +407,9 @@ public class PeopleBackupHelper extends SharedPreferencesBackupHelper { private List<String> getExistingWidgetsForUser(int userId) { List<String> existingWidgets = new ArrayList<>(); + if (mAppWidgetManager == null) { + return existingWidgets; + } int[] ids = mAppWidgetManager.getAppWidgetIds( new ComponentName(mContext, PeopleSpaceWidgetProvider.class)); for (int id : ids) { @@ -491,7 +497,11 @@ public class PeopleBackupHelper extends SharedPreferencesBackupHelper { /** Sends a broadcast to update the existing Conversation widgets. */ public static void updateWidgets(Context context) { - int[] widgetIds = AppWidgetManager.getInstance(context) + AppWidgetManager manager = AppWidgetManager.getInstance(context); + if (manager == null) { + return; + } + int[] widgetIds = manager .getAppWidgetIds(new ComponentName(context, PeopleSpaceWidgetProvider.class)); if (DEBUG) { for (int id : widgetIds) { diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java index 0a880293ca76..b0de80c8d7bb 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java @@ -140,7 +140,7 @@ public class PeopleSpaceWidgetManager implements Dumpable { private final Object mLock = new Object(); private final Context mContext; private LauncherApps mLauncherApps; - private AppWidgetManager mAppWidgetManager; + private Optional<AppWidgetManager> mAppWidgetManagerOptional; private IPeopleManager mIPeopleManager; private SharedPreferences mSharedPrefs; private PeopleManager mPeopleManager; @@ -183,8 +183,9 @@ public class PeopleSpaceWidgetManager implements Dumpable { }; @Inject - public PeopleSpaceWidgetManager(Context context, LauncherApps launcherApps, - CommonNotifCollection notifCollection, + public PeopleSpaceWidgetManager(Context context, + Optional<AppWidgetManager> appWidgetManagerOptional, + LauncherApps launcherApps, CommonNotifCollection notifCollection, PackageManager packageManager, Optional<Bubbles> bubblesOptional, UserManager userManager, NotificationManager notificationManager, BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor, @@ -192,7 +193,7 @@ public class PeopleSpaceWidgetManager implements Dumpable { @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor) { if (DEBUG) Log.d(TAG, "constructor"); mContext = context; - mAppWidgetManager = AppWidgetManager.getInstance(context); + mAppWidgetManagerOptional = appWidgetManagerOptional; mIPeopleManager = IPeopleManager.Stub.asInterface( ServiceManager.getService(Context.PEOPLE_SERVICE)); mLauncherApps = launcherApps; @@ -266,14 +267,14 @@ public class PeopleSpaceWidgetManager implements Dumpable { */ @VisibleForTesting PeopleSpaceWidgetManager(Context context, - AppWidgetManager appWidgetManager, IPeopleManager iPeopleManager, + Optional<AppWidgetManager> appWidgetManager, IPeopleManager iPeopleManager, PeopleManager peopleManager, LauncherApps launcherApps, CommonNotifCollection notifCollection, PackageManager packageManager, Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager, INotificationManager iNotificationManager, NotificationManager notificationManager, @Background Executor executor, UserTracker userTracker) { mContext = context; - mAppWidgetManager = appWidgetManager; + mAppWidgetManagerOptional = appWidgetManager; mIPeopleManager = iPeopleManager; mPeopleManager = peopleManager; mLauncherApps = launcherApps; @@ -337,6 +338,10 @@ public class PeopleSpaceWidgetManager implements Dumpable { /** Updates the current widget view with provided {@link PeopleSpaceTile}. */ private void updateAppWidgetViews(int appWidgetId, PeopleSpaceTile tile, Bundle options) { + if (mAppWidgetManagerOptional.isEmpty()) { + return; + } + PeopleTileKey key = getKeyFromStorageByWidgetId(appWidgetId); if (DEBUG) Log.d(TAG, "Widget: " + appWidgetId + " for: " + key.toString()); @@ -349,7 +354,7 @@ public class PeopleSpaceWidgetManager implements Dumpable { // Tell the AppWidgetManager to perform an update on the current app widget. if (DEBUG) Log.d(TAG, "Calling update widget for widgetId: " + appWidgetId); - mAppWidgetManager.updateAppWidget(appWidgetId, views); + mAppWidgetManagerOptional.get().updateAppWidget(appWidgetId, views); } /** Updates tile in app widget options and the current view. */ @@ -362,13 +367,17 @@ public class PeopleSpaceWidgetManager implements Dumpable { /** Updates tile in app widget options and the current view. */ public void updateAppWidgetOptionsAndView(int appWidgetId, PeopleSpaceTile tile) { + if (mAppWidgetManagerOptional.isEmpty()) { + return; + } + if (tile == null) { Log.w(TAG, "Storing null tile for widget " + appWidgetId); } synchronized (mTiles) { mTiles.put(appWidgetId, tile); } - Bundle options = mAppWidgetManager.getAppWidgetOptions(appWidgetId); + Bundle options = mAppWidgetManagerOptional.get().getAppWidgetOptions(appWidgetId); updateAppWidgetViews(appWidgetId, tile, options); } @@ -484,6 +493,10 @@ public class PeopleSpaceWidgetManager implements Dumpable { private void updateWidgetsWithNotificationChangedInBackground(StatusBarNotification sbn, PeopleSpaceUtils.NotificationAction action, Collection<NotificationEntry> notifications) { + if (mAppWidgetManagerOptional.isEmpty()) { + return; + } + try { PeopleTileKey key = new PeopleTileKey( sbn.getShortcutId(), sbn.getUser().getIdentifier(), sbn.getPackageName()); @@ -491,7 +504,7 @@ public class PeopleSpaceWidgetManager implements Dumpable { if (DEBUG) Log.d(TAG, "Sbn doesn't contain valid PeopleTileKey: " + key.toString()); return; } - int[] widgetIds = mAppWidgetManager.getAppWidgetIds( + int[] widgetIds = mAppWidgetManagerOptional.get().getAppWidgetIds( new ComponentName(mContext, PeopleSpaceWidgetProvider.class) ); if (widgetIds.length == 0) { @@ -807,10 +820,14 @@ public class PeopleSpaceWidgetManager implements Dumpable { UserHandle user, NotificationChannel channel, int modificationType) { + if (mAppWidgetManagerOptional.isEmpty()) { + return; + } + if (channel.isConversation()) { mBgExecutor.execute(() -> { if (mUserManager.isUserUnlocked(user)) { - updateWidgets(mAppWidgetManager.getAppWidgetIds( + updateWidgets(mAppWidgetManagerOptional.get().getAppWidgetIds( new ComponentName(mContext, PeopleSpaceWidgetProvider.class) )); } @@ -829,13 +846,18 @@ public class PeopleSpaceWidgetManager implements Dumpable { // learning about the widget. If so, the widget adder should have populated options with // PeopleTileKey arguments. if (DEBUG) Log.d(TAG, "onAppWidgetOptionsChanged called for widget: " + appWidgetId); + if (mAppWidgetManagerOptional.isEmpty()) { + return; + } + PeopleTileKey optionsKey = AppWidgetOptionsHelper.getPeopleTileKeyFromBundle(newOptions); if (PeopleTileKey.isValid(optionsKey)) { if (DEBUG) { Log.d(TAG, "PeopleTileKey was present in Options, shortcutId: " + optionsKey.getShortcutId()); } - AppWidgetOptionsHelper.removePeopleTileKey(mAppWidgetManager, appWidgetId); + AppWidgetOptionsHelper.removePeopleTileKey(mAppWidgetManagerOptional.get(), + appWidgetId); addNewWidget(appWidgetId, optionsKey); } // Update views for new widget dimensions. @@ -1004,6 +1026,10 @@ public class PeopleSpaceWidgetManager implements Dumpable { public boolean requestPinAppWidget(ShortcutInfo shortcutInfo, Bundle options) { if (DEBUG) Log.d(TAG, "Requesting pin widget, shortcutId: " + shortcutInfo.getId()); + if (mAppWidgetManagerOptional.isEmpty()) { + return false; + } + RemoteViews widgetPreview = getPreview(shortcutInfo.getId(), shortcutInfo.getUserHandle(), shortcutInfo.getPackage(), options); if (widgetPreview == null) { @@ -1017,7 +1043,8 @@ public class PeopleSpaceWidgetManager implements Dumpable { PeopleSpaceWidgetPinnedReceiver.getPendingIntent(mContext, shortcutInfo); ComponentName componentName = new ComponentName(mContext, PeopleSpaceWidgetProvider.class); - return mAppWidgetManager.requestPinAppWidget(componentName, extras, successCallback); + return mAppWidgetManagerOptional.get().requestPinAppWidget(componentName, extras, + successCallback); } /** Returns a list of map entries corresponding to user's priority conversations. */ @@ -1104,7 +1131,11 @@ public class PeopleSpaceWidgetManager implements Dumpable { /** Updates any app widget to the current state, triggered by a broadcast update. */ @VisibleForTesting void updateWidgetsFromBroadcastInBackground(String entryPoint) { - int[] appWidgetIds = mAppWidgetManager.getAppWidgetIds( + if (mAppWidgetManagerOptional.isEmpty()) { + return; + } + + int[] appWidgetIds = mAppWidgetManagerOptional.get().getAppWidgetIds( new ComponentName(mContext, PeopleSpaceWidgetProvider.class)); if (appWidgetIds == null) { return; @@ -1272,13 +1303,17 @@ public class PeopleSpaceWidgetManager implements Dumpable { remapSharedFile(widgets); remapFollowupFile(widgets); - int[] widgetIds = mAppWidgetManager.getAppWidgetIds( + if (mAppWidgetManagerOptional.isEmpty()) { + return; + } + + int[] widgetIds = mAppWidgetManagerOptional.get().getAppWidgetIds( new ComponentName(mContext, PeopleSpaceWidgetProvider.class)); Bundle b = new Bundle(); b.putBoolean(AppWidgetManager.OPTION_APPWIDGET_RESTORE_COMPLETED, true); for (int id : widgetIds) { if (DEBUG) Log.d(TAG, "Setting widget as restored, widget id:" + id); - mAppWidgetManager.updateAppWidgetOptions(id, b); + mAppWidgetManagerOptional.get().updateAppWidgetOptions(id, b); } updateWidgets(widgetIds); @@ -1437,14 +1472,15 @@ public class PeopleSpaceWidgetManager implements Dumpable { @VisibleForTesting void updateGeneratedPreviewForUser(UserHandle user) { if (!generatedPreviews() || mUpdatedPreviews.get(user.getIdentifier()) - || !mUserManager.isUserUnlocked(user)) { + || !mUserManager.isUserUnlocked(user) || mAppWidgetManagerOptional.isEmpty()) { return; } // The widget provider may be disabled on SystemUI implementers, e.g. TvSystemUI. ComponentName provider = new ComponentName(mContext, PeopleSpaceWidgetProvider.class); - List<AppWidgetProviderInfo> infos = mAppWidgetManager.getInstalledProvidersForPackage( - mContext.getPackageName(), user); + List<AppWidgetProviderInfo> infos = + mAppWidgetManagerOptional.get().getInstalledProvidersForPackage( + mContext.getPackageName(), user); if (infos.stream().noneMatch(info -> info.provider.equals(provider))) { return; } @@ -1452,7 +1488,7 @@ public class PeopleSpaceWidgetManager implements Dumpable { if (DEBUG) { Log.d(TAG, "Updating People Space widget preview for user " + user.getIdentifier()); } - boolean success = mAppWidgetManager.setWidgetPreview( + boolean success = mAppWidgetManagerOptional.get().setWidgetPreview( provider, WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD, new RemoteViews(mContext.getPackageName(), R.layout.people_space_placeholder_layout)); diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java index 10c8e530553a..cf1dca3cbb23 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java @@ -16,6 +16,8 @@ package com.android.systemui.qs; +import android.content.res.Resources; + import com.android.systemui.statusbar.policy.CallbackController; public interface ReduceBrightColorsController extends @@ -27,6 +29,14 @@ public interface ReduceBrightColorsController extends /** Sets the activation state of Reduce Bright Colors */ void setReduceBrightColorsActivated(boolean activated); + /** Sets whether Reduce Bright Colors is enabled */ + void setReduceBrightColorsFeatureAvailable(boolean enabled); + + /** Gets whether Reduce Bright Colors is enabled */ + boolean isReduceBrightColorsFeatureAvailable(); + + /** Gets whether Reduce Bright Colors is being transitioned to Even Dimmer */ + boolean isInUpgradeMode(Resources resources); /** * Listener invoked whenever the Reduce Bright Colors settings are changed. */ @@ -38,5 +48,12 @@ public interface ReduceBrightColorsController extends */ default void onActivated(boolean activated) { } + /** + * Listener invoked when the feature enabled state changes. + * + * @param enabled {@code true} if Reduce Bright Colors feature is enabled. + */ + default void onFeatureEnabledChanged(boolean enabled) { + } } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java index 846d63f10875..d68b22b84f09 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java @@ -19,6 +19,7 @@ package com.android.systemui.qs; import android.content.Context; +import android.content.res.Resources; import android.database.ContentObserver; import android.hardware.display.ColorDisplayManager; import android.net.Uri; @@ -28,6 +29,7 @@ import android.provider.Settings; import androidx.annotation.NonNull; +import com.android.server.display.feature.flags.Flags; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.settings.UserTracker; @@ -47,6 +49,7 @@ public class ReduceBrightColorsControllerImpl implements private final ContentObserver mContentObserver; private final SecureSettings mSecureSettings; private final ArrayList<ReduceBrightColorsController.Listener> mListeners = new ArrayList<>(); + private boolean mAvailable = true; @Inject public ReduceBrightColorsControllerImpl(UserTracker userTracker, @@ -75,6 +78,7 @@ public class ReduceBrightColorsControllerImpl implements mCurrentUserTrackerCallback = new UserTracker.Callback() { @Override public void onUserChanged(int newUser, Context userContext) { + mAvailable = true; synchronized (mListeners) { if (mListeners.size() > 0) { mSecureSettings.unregisterContentObserverSync(mContentObserver); @@ -121,10 +125,35 @@ public class ReduceBrightColorsControllerImpl implements mManager.setReduceBrightColorsActivated(activated); } + @Override + public void setReduceBrightColorsFeatureAvailable(boolean enabled) { + mAvailable = enabled; + dispatchOnEnabledChanged(enabled); + mAvailable = true; + } + + @Override + public boolean isReduceBrightColorsFeatureAvailable() { + return mAvailable; + } + + @Override + public boolean isInUpgradeMode(Resources resources) { + return Flags.evenDimmer() && resources.getBoolean( + com.android.internal.R.bool.config_evenDimmerEnabled); + } + private void dispatchOnActivated(boolean activated) { ArrayList<Listener> copy = new ArrayList<>(mListeners); for (Listener l : copy) { l.onActivated(activated); } } + + private void dispatchOnEnabledChanged(boolean enabled) { + ArrayList<Listener> copy = new ArrayList<>(mListeners); + for (Listener l : copy) { + l.onFeatureEnabledChanged(enabled); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java b/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java index 6092348b964e..2287f4d68933 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java +++ b/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java @@ -19,6 +19,7 @@ package com.android.systemui.qs; import android.database.ContentObserver; import android.os.Handler; +import com.android.systemui.Flags; import com.android.systemui.statusbar.policy.Listenable; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.settings.SettingsProxy; @@ -74,10 +75,20 @@ public abstract class SettingObserver extends ContentObserver implements Listena mListening = listening; if (listening) { mObservedValue = getValueFromProvider(); - mSettingsProxy.registerContentObserverSync( - mSettingsProxy.getUriFor(mSettingName), false, this); + if (Flags.qsRegisterSettingObserverOnBgThread()) { + mSettingsProxy.registerContentObserverAsync( + mSettingsProxy.getUriFor(mSettingName), false, this, + () -> mObservedValue = getValueFromProvider()); + } else { + mSettingsProxy.registerContentObserverSync( + mSettingsProxy.getUriFor(mSettingName), false, this); + } } else { - mSettingsProxy.unregisterContentObserverSync(this); + if (Flags.qsRegisterSettingObserverOnBgThread()) { + mSettingsProxy.unregisterContentObserverAsync(this); + } else { + mSettingsProxy.unregisterContentObserverSync(this); + } mObservedValue = mDefaultValue; } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index a45d6f63cd81..89f85ab14dd6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -32,6 +32,7 @@ import android.widget.Button; import androidx.annotation.Nullable; +import com.android.server.display.feature.flags.Flags; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.qs.QSTile; @@ -116,6 +117,10 @@ public class TileQueryHelper { final ArrayList<QSTile> tilesToAdd = new ArrayList<>(); possibleTiles.remove("cell"); possibleTiles.remove("wifi"); + if (Flags.evenDimmer() && mContext.getResources().getBoolean( + com.android.internal.R.bool.config_evenDimmerEnabled)) { + possibleTiles.remove("reduce_brightness"); + } for (String spec : possibleTiles) { // Only add current and stock tiles that can be created from QSFactoryImpl. diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt index ec9d151a26d3..86a29f91e51c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.panels.data.repository import android.content.res.Resources +import com.android.server.display.feature.flags.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.pipeline.shared.TileSpec @@ -32,10 +33,15 @@ constructor( /** * List of stock platform tiles. All of the specs will be of type [TileSpec.PlatformTileSpec]. */ + val shouldRemoveRbcTile: Boolean = + Flags.evenDimmer() && + resources.getBoolean(com.android.internal.R.bool.config_evenDimmerEnabled) + val stockTiles = resources .getString(R.string.quick_settings_tiles_stock) .split(",") + .filterNot { shouldRemoveRbcTile && it.equals("reduce_brightness") } .map(TileSpec::create) .filterNot { it is TileSpec.Invalid } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt index 2f87b01e6b61..3fdd7f769cf3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt @@ -22,7 +22,6 @@ import android.graphics.drawable.Animatable import android.service.quicksettings.Tile.STATE_ACTIVE import android.service.quicksettings.Tile.STATE_INACTIVE import android.text.TextUtils -import android.util.Log import androidx.appcompat.content.res.AppCompatResources import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi import androidx.compose.animation.graphics.res.animatedVectorResource @@ -465,7 +464,6 @@ private fun TileIcon( animateToEnd: Boolean = false, modifier: Modifier = Modifier, ) { - Log.d("Fabian", "Recomposing tile icon") val iconModifier = modifier.size(dimensionResource(id = R.dimen.qs_icon_size)) val context = LocalContext.current val loadedDrawable = diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt index 9c1b85799648..4d823ab216f0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt @@ -38,10 +38,11 @@ class ReduceBrightColorsAutoAddable @Inject constructor( controller: ReduceBrightColorsController, - @Named(RBC_AVAILABLE) private val available: Boolean, + @Named(RBC_AVAILABLE) private var available: Boolean, ) : CallbackControllerAutoAddable< - ReduceBrightColorsController.Listener, ReduceBrightColorsController + ReduceBrightColorsController.Listener, + ReduceBrightColorsController >(controller) { override val spec: TileSpec @@ -50,10 +51,16 @@ constructor( override fun ProducerScope<AutoAddSignal>.getCallback(): ReduceBrightColorsController.Listener { return object : ReduceBrightColorsController.Listener { override fun onActivated(activated: Boolean) { - if (activated) { + if (activated && available) { sendAdd() } } + + override fun onFeatureEnabledChanged(enabled: Boolean) { + if (!enabled) { + available = false + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 44c846b5c631..787fd1ab7170 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -386,6 +386,7 @@ constructor( // The launch animation of a long-press effect did not reset the long-press effect so // we must do it here resetLongPressEffectProperties() + longPressEffect.resetState() } val actualHeight = if (heightOverride != HeightOverrideable.NO_OVERRIDE) { @@ -771,11 +772,14 @@ constructor( lastIconTint = icon.getColor(state) // Long-press effects + longPressEffect?.qsTile?.state?.handlesLongClick = state.handlesLongClick if ( state.handlesLongClick && longPressEffect?.initializeEffect(longPressEffectDuration) == true ) { showRippleEffect = false + longPressEffect.qsTile?.state?.state = lastState // Store the tile's state + longPressEffect.resetState() initializeLongPressProperties(measuredHeight, measuredWidth) } else { // Long-press effects might have been enabled before but the new state does not @@ -906,6 +910,7 @@ constructor( } override fun onActivityLaunchAnimationEnd() { + longPressEffect?.resetState() if (longPressEffect != null && !haveLongPressPropertiesBeenReset) { resetLongPressEffectProperties() } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt index f34389ec8cb5..53594bbb2c84 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt @@ -20,12 +20,14 @@ import com.android.systemui.res.R /** Return the subtitle resource Id of the given tile. */ object SubtitleArrayMapping { private val subtitleIdsMap: HashMap<String, Int> = HashMap() + init { subtitleIdsMap["internet"] = R.array.tile_states_internet subtitleIdsMap["wifi"] = R.array.tile_states_wifi subtitleIdsMap["cell"] = R.array.tile_states_cell subtitleIdsMap["battery"] = R.array.tile_states_battery subtitleIdsMap["dnd"] = R.array.tile_states_dnd + subtitleIdsMap["modes"] = R.array.tile_states_modes subtitleIdsMap["flashlight"] = R.array.tile_states_flashlight subtitleIdsMap["rotation"] = R.array.tile_states_rotation subtitleIdsMap["bt"] = R.array.tile_states_bt diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java index 34723523b84f..af5b31180159 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java @@ -48,12 +48,13 @@ import javax.inject.Named; /** Quick settings tile: Reduce Bright Colors **/ public class ReduceBrightColorsTile extends QSTileImpl<QSTile.BooleanState> - implements ReduceBrightColorsController.Listener{ + implements ReduceBrightColorsController.Listener { public static final String TILE_SPEC = "reduce_brightness"; - private final boolean mIsAvailable; + private boolean mIsAvailable; private final ReduceBrightColorsController mReduceBrightColorsController; private boolean mIsListening; + private final boolean mInUpgradeMode; @Inject public ReduceBrightColorsTile( @@ -73,9 +74,11 @@ public class ReduceBrightColorsTile extends QSTileImpl<QSTile.BooleanState> statusBarStateController, activityStarter, qsLogger); mReduceBrightColorsController = reduceBrightColorsController; mReduceBrightColorsController.observe(getLifecycle(), this); - mIsAvailable = isAvailable; + mInUpgradeMode = reduceBrightColorsController.isInUpgradeMode(mContext.getResources()); + mIsAvailable = isAvailable || mInUpgradeMode; } + @Override public boolean isAvailable() { return mIsAvailable; @@ -93,12 +96,28 @@ public class ReduceBrightColorsTile extends QSTileImpl<QSTile.BooleanState> @Override public Intent getLongClickIntent() { - return new Intent(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS); + return goToEvenDimmer() ? new Intent(Settings.ACTION_DISPLAY_SETTINGS) : new Intent( + Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS); + } + + private boolean goToEvenDimmer() { + if (mInUpgradeMode) { + mHost.removeTile(getTileSpec()); + mIsAvailable = false; + return true; + } + return false; } @Override protected void handleClick(@Nullable Expandable expandable) { - mReduceBrightColorsController.setReduceBrightColorsActivated(!mState.value); + + if (goToEvenDimmer()) { + mActivityStarter.postStartActivityDismissingKeyguard( + new Intent(Settings.ACTION_DISPLAY_SETTINGS), 0); + } else { + mReduceBrightColorsController.setReduceBrightColorsActivated(!mState.value); + } } @Override @@ -127,4 +146,9 @@ public class ReduceBrightColorsTile extends QSTileImpl<QSTile.BooleanState> public void onActivated(boolean activated) { refreshState(); } + + @Override + public void onFeatureEnabledChanged(boolean enabled) { + refreshState(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt new file mode 100644 index 000000000000..da4d2f1c0085 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.modes.domain.interactor + +import android.app.Flags +import android.os.UserHandle +import android.provider.Settings.Global.ZEN_MODE_OFF +import com.android.settingslib.notification.data.repository.ZenModeRepository +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +class ModesTileDataInteractor @Inject constructor(val zenModeRepository: ZenModeRepository) : + QSTileDataInteractor<ModesTileModel> { + // TODO(b/346519570): This should be checking for any enabled modes. + private val zenModeActive = + zenModeRepository.globalZenMode.map { it != ZEN_MODE_OFF }.distinctUntilChanged() + + override fun tileData( + user: UserHandle, + triggers: Flow<DataUpdateTrigger> + ): Flow<ModesTileModel> { + return zenModeActive.map { ModesTileModel(isActivated = it) } + } + + override fun availability(user: UserHandle): Flow<Boolean> = flowOf(Flags.modesUi()) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt new file mode 100644 index 000000000000..e2fea84eced4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.modes.domain.interactor + +import com.android.systemui.qs.tiles.base.interactor.QSTileInput +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import javax.inject.Inject + +class ModesTileUserActionInteractor @Inject constructor() : + QSTileUserActionInteractor<ModesTileModel> { + override suspend fun handleInput(input: QSTileInput<ModesTileModel>) { + with(input) { + when (action) { + is QSTileUserAction.Click -> { + // TODO(b/346519570) open dialog + } + is QSTileUserAction.LongClick -> { + // TODO(b/346519570) open settings + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt new file mode 100644 index 000000000000..e44413a962f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.modes.domain.model +data class ModesTileModel(val isActivated: Boolean) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt new file mode 100644 index 000000000000..07b393e549c2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.modes.ui + +import android.content.res.Resources +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import javax.inject.Inject + +class ModesTileMapper +@Inject +constructor( + @Main private val resources: Resources, + val theme: Resources.Theme, +) : QSTileDataToStateMapper<ModesTileModel> { + override fun map(config: QSTileConfig, data: ModesTileModel): QSTileState = + QSTileState.build(resources, theme, config.uiConfig) { + val iconRes = + if (data.isActivated) { + R.drawable.qs_dnd_icon_on + } else { + R.drawable.qs_dnd_icon_off + } + val icon = Icon.Loaded(resources.getDrawable(iconRes, theme), contentDescription = null) + this.icon = { icon } + if (data.isActivated) { + activationState = QSTileState.ActivationState.ACTIVE + secondaryLabel = "Some modes enabled idk" // TODO(b/346519570) + } else { + activationState = QSTileState.ActivationState.INACTIVE + secondaryLabel = "Off" // TODO(b/346519570) + } + contentDescription = label + supportedActions = + setOf( + QSTileState.UserAction.CLICK, + QSTileState.UserAction.LONG_CLICK, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt index 98fd561b27d6..00b1e41461f5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt @@ -23,13 +23,13 @@ import com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.reducebrightness.domain.model.ReduceBrightColorsTileModel +import com.android.systemui.util.kotlin.isAvailable import com.android.systemui.util.kotlin.isEnabled import javax.inject.Inject import javax.inject.Named import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -52,5 +52,7 @@ constructor( .map { ReduceBrightColorsTileModel(it) } .flowOn(bgCoroutineContext) } - override fun availability(user: UserHandle): Flow<Boolean> = flowOf(isAvailable) + + override fun availability(user: UserHandle): Flow<Boolean> = + reduceBrightColorsController.isAvailable() } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt index 14dbe0e8a69a..ed5e4fe74962 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt @@ -17,7 +17,9 @@ package com.android.systemui.qs.tiles.impl.reducebrightness.domain.interactor import android.content.Intent +import android.content.res.Resources import android.provider.Settings +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.ReduceBrightColorsController import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput @@ -30,19 +32,40 @@ import javax.inject.Inject class ReduceBrightColorsTileUserActionInteractor @Inject constructor( + @Main private val resources: Resources, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, private val reduceBrightColorsController: ReduceBrightColorsController, ) : QSTileUserActionInteractor<ReduceBrightColorsTileModel> { + val isInUpgradeMode: Boolean = reduceBrightColorsController.isInUpgradeMode(resources) + override suspend fun handleInput(input: QSTileInput<ReduceBrightColorsTileModel>): Unit = with(input) { when (action) { is QSTileUserAction.Click -> { + if (isInUpgradeMode) { + reduceBrightColorsController.setReduceBrightColorsFeatureAvailable(false) + qsTileIntentUserActionHandler.handle( + action.expandable, + Intent(Settings.ACTION_DISPLAY_SETTINGS) + ) + // TODO(b/349458355): show dialog + return@with + } reduceBrightColorsController.setReduceBrightColorsActivated( !input.data.isEnabled ) } is QSTileUserAction.LongClick -> { + if (isInUpgradeMode) { + reduceBrightColorsController.setReduceBrightColorsFeatureAvailable(false) + qsTileIntentUserActionHandler.handle( + action.expandable, + Intent(Settings.ACTION_DISPLAY_SETTINGS) + ) + // TODO(b/349458355): show dialog + return@with + } qsTileIntentUserActionHandler.handle( action.expandable, Intent(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt index 1868b4a29f20..54e0319da58f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt @@ -40,7 +40,7 @@ constructor( private val intentExecutor: ActionIntentExecutor, @Application private val applicationScope: CoroutineScope, @Assisted val window: Window, - @Assisted val viewProxy: ScreenshotViewProxy, + @Assisted val viewProxy: ScreenshotShelfViewProxy, @Assisted val finishDismiss: () -> Unit, ) { @@ -109,7 +109,7 @@ constructor( interface Factory { fun create( window: Window, - viewProxy: ScreenshotViewProxy, + viewProxy: ScreenshotShelfViewProxy, finishDismiss: (() -> Unit) ): ActionExecutor } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt deleted file mode 100644 index 3d024a6a8ccf..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt +++ /dev/null @@ -1,242 +0,0 @@ -/* - * 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.screenshot - -import android.animation.Animator -import android.app.Notification -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Rect -import android.util.Log -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.ScrollCaptureResponse -import android.view.View -import android.view.ViewTreeObserver -import android.view.WindowInsets -import android.window.OnBackInvokedCallback -import android.window.OnBackInvokedDispatcher -import androidx.appcompat.content.res.AppCompatResources -import com.android.internal.logging.UiEventLogger -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.res.R -import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS -import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER -import com.android.systemui.screenshot.scroll.ScrollCaptureController -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -/** - * Legacy implementation of screenshot view methods. Just proxies the calls down into the original - * ScreenshotView. - */ -class LegacyScreenshotViewProxy -@AssistedInject -constructor( - private val logger: UiEventLogger, - flags: FeatureFlags, - @Assisted private val context: Context, - @Assisted private val displayId: Int -) : ScreenshotViewProxy { - override val view: ScreenshotView = - LayoutInflater.from(context).inflate(R.layout.screenshot, null) as ScreenshotView - override val screenshotPreview: View - override var packageName: String = "" - set(value) { - field = value - view.setPackageName(value) - } - override var callbacks: ScreenshotView.ScreenshotViewCallback? = null - set(value) { - field = value - view.setCallbacks(value) - } - override var screenshot: ScreenshotData? = null - set(value) { - field = value - value?.let { - val badgeBg = - AppCompatResources.getDrawable(context, R.drawable.overlay_badge_background) - val user = it.userHandle - if (badgeBg != null && user != null) { - view.badgeScreenshot(context.packageManager.getUserBadgedIcon(badgeBg, user)) - } - view.setScreenshot(it) - } - } - - override val isAttachedToWindow - get() = view.isAttachedToWindow - override val isDismissing - get() = view.isDismissing - override val isPendingSharedTransition - get() = view.isPendingSharedTransition - - init { - view.setUiEventLogger(logger) - view.setDefaultDisplay(displayId) - view.setFlags(flags) - addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } - setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } - if (LogConfig.DEBUG_WINDOW) { - Log.d(TAG, "adding OnComputeInternalInsetsListener") - } - view.viewTreeObserver.addOnComputeInternalInsetsListener(view) - screenshotPreview = view.screenshotPreview - } - - override fun reset() = view.reset() - override fun updateInsets(insets: WindowInsets) = view.updateInsets(insets) - override fun updateOrientation(insets: WindowInsets) = view.updateOrientation(insets) - - override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator = - view.createScreenshotDropInAnimation(screenRect, showFlash) - - override fun addQuickShareChip(quickShareAction: Notification.Action) = - view.addQuickShareChip(quickShareAction) - - override fun setChipIntents(imageData: ScreenshotController.SavedImageData) = - view.setChipIntents(imageData) - - override fun requestDismissal(event: ScreenshotEvent?) { - if (DEBUG_DISMISS) { - Log.d(TAG, "screenshot dismissal requested") - } - // If we're already animating out, don't restart the animation - if (view.isDismissing) { - if (DEBUG_DISMISS) { - Log.v(TAG, "Already dismissing, ignoring duplicate command $event") - } - return - } - event?.let { logger.log(event, 0, packageName) } - view.animateDismissal() - } - - override fun showScrollChip(packageName: String, onClick: Runnable) = - view.showScrollChip(packageName, onClick) - - override fun hideScrollChip() = view.hideScrollChip() - - override fun prepareScrollingTransition( - response: ScrollCaptureResponse, - screenBitmap: Bitmap, - newScreenshot: Bitmap, - screenshotTakenInPortrait: Boolean, - onTransitionPrepared: Runnable, - ) { - view.prepareScrollingTransition( - response, - screenBitmap, - newScreenshot, - screenshotTakenInPortrait - ) - view.post { onTransitionPrepared.run() } - } - - override fun startLongScreenshotTransition( - transitionDestination: Rect, - onTransitionEnd: Runnable, - longScreenshot: ScrollCaptureController.LongScreenshot - ) = view.startLongScreenshotTransition(transitionDestination, onTransitionEnd, longScreenshot) - - override fun restoreNonScrollingUi() = view.restoreNonScrollingUi() - - override fun fadeForSharedTransition() {} // unused - - override fun stopInputListening() = view.stopInputListening() - - override fun requestFocus() { - view.requestFocus() - } - - override fun announceForAccessibility(string: String) = view.announceForAccessibility(string) - - override fun prepareEntranceAnimation(runnable: Runnable) { - view.viewTreeObserver.addOnPreDrawListener( - object : ViewTreeObserver.OnPreDrawListener { - override fun onPreDraw(): Boolean { - if (LogConfig.DEBUG_WINDOW) { - Log.d(TAG, "onPreDraw: startAnimation") - } - view.viewTreeObserver.removeOnPreDrawListener(this) - runnable.run() - return true - } - } - ) - } - - private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) { - val onBackInvokedCallback = OnBackInvokedCallback { - if (LogConfig.DEBUG_INPUT) { - Log.d(TAG, "Predictive Back callback dispatched") - } - onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) - } - view.addOnAttachStateChangeListener( - object : View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View) { - if (LogConfig.DEBUG_INPUT) { - Log.d(TAG, "Registering Predictive Back callback") - } - view - .findOnBackInvokedDispatcher() - ?.registerOnBackInvokedCallback( - OnBackInvokedDispatcher.PRIORITY_DEFAULT, - onBackInvokedCallback - ) - } - - override fun onViewDetachedFromWindow(view: View) { - if (LogConfig.DEBUG_INPUT) { - Log.d(TAG, "Unregistering Predictive Back callback") - } - view - .findOnBackInvokedDispatcher() - ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) - } - } - ) - } - private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) { - view.setOnKeyListener( - object : View.OnKeyListener { - override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean { - if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { - if (LogConfig.DEBUG_INPUT) { - Log.d(TAG, "onKeyEvent: $keyCode") - } - onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) - return true - } - return false - } - } - ) - } - - @AssistedFactory - interface Factory : ScreenshotViewProxy.Factory { - override fun getProxy(context: Context, displayId: Int): LegacyScreenshotViewProxy - } - - companion object { - private const val TAG = "LegacyScreenshotViewProxy" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt index 596046268984..474afa8bcb9d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt @@ -137,7 +137,7 @@ constructor( val offset = container.height + params.topMargin + params.bottomMargin val anim = if (animateIn) ValueAnimator.ofFloat(0f, 1f) else ValueAnimator.ofFloat(1f, 0f) with(anim) { - duration = ScreenshotView.SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS + duration = MESSAGE_EXPANSION_DURATION_MS interpolator = AccelerateDecelerateInterpolator() addUpdateListener { valueAnimator: ValueAnimator -> val interpolation = valueAnimator.animatedValue as Float @@ -147,4 +147,8 @@ constructor( } return anim } + + companion object { + const val MESSAGE_EXPANSION_DURATION_MS: Long = 400 + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 95ee2e06817b..773900981759 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -20,6 +20,7 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static com.android.systemui.Flags.screenshotPrivateProfileAccessibilityAnnouncementFix; +import static com.android.systemui.Flags.screenshotSaveImageExporter; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; @@ -190,7 +191,7 @@ public class ScreenshotController implements ScreenshotHandler { private final WindowContext mContext; private final FeatureFlags mFlags; - private final ScreenshotViewProxy mViewProxy; + private final ScreenshotShelfViewProxy mViewProxy; private final ScreenshotNotificationsController mNotificationsController; private final ScreenshotSmartActions mScreenshotSmartActions; private final UiEventLogger mUiEventLogger; @@ -230,13 +231,6 @@ public class ScreenshotController implements ScreenshotHandler { private String mPackageName = ""; private final BroadcastReceiver mCopyBroadcastReceiver; - // When false, the screenshot is taken without showing the ui. Note that this only applies to - // external displays, as on the default one the UI should **always** be shown. - // This is needed in case of screenshot during display mirroring, as adding another window to - // the external display makes mirroring stop. - // When there is a way to distinguish between displays that are mirroring or extending, this - // can be removed and we can directly show the ui only in the extended case. - private final Boolean mShowUIOnExternalDisplay; /** Tracks config changes that require re-creating UI */ private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges( ActivityInfo.CONFIG_ORIENTATION @@ -252,7 +246,7 @@ public class ScreenshotController implements ScreenshotHandler { Context context, WindowManager windowManager, FeatureFlags flags, - ScreenshotViewProxy.Factory viewProxyFactory, + ScreenshotShelfViewProxy.Factory viewProxyFactory, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory, UiEventLogger uiEventLogger, @@ -272,8 +266,7 @@ public class ScreenshotController implements ScreenshotHandler { MessageContainerController messageContainerController, Provider<ScreenshotSoundController> screenshotSoundController, AnnouncementResolver announcementResolver, - @Assisted Display display, - @Assisted boolean showUIOnExternalDisplay + @Assisted Display display ) { mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsControllerFactory.create( @@ -347,7 +340,6 @@ public class ScreenshotController implements ScreenshotHandler { mBroadcastDispatcher.registerReceiver(mCopyBroadcastReceiver, new IntentFilter( ClipboardOverlayController.COPY_OVERLAY_ACTION), null, null, Context.RECEIVER_NOT_EXPORTED, ClipboardOverlayController.SELF_PERMISSION); - mShowUIOnExternalDisplay = showUIOnExternalDisplay; } @Override @@ -381,7 +373,7 @@ public class ScreenshotController implements ScreenshotHandler { Log.w(TAG, "User setup not complete, displaying toast only"); // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing // and sharing shouldn't be exposed to the user. - saveScreenshotAndToast(screenshot.getUserHandle(), finisher); + saveScreenshotAndToast(screenshot, finisher); return; } @@ -397,17 +389,15 @@ public class ScreenshotController implements ScreenshotHandler { prepareViewForNewScreenshot(screenshot, oldPackageName); - if (!shouldShowUi()) { - saveScreenshotInWorkerThread( - screenshot.getUserHandle(), finisher, this::logSuccessOnActionsReady, - (ignored) -> { - }); - return; - } - final UUID requestId; requestId = mActionsController.setCurrentScreenshot(screenshot); - saveScreenshotInBackground(screenshot, requestId, finisher); + saveScreenshotInBackground(screenshot, requestId, finisher, result -> { + if (result.uri != null) { + ScreenshotSavedResult savedScreenshot = new ScreenshotSavedResult( + result.uri, screenshot.getUserOrDefault(), result.timestamp); + mActionsController.setCompletedScreenshot(requestId, savedScreenshot); + } + }); if (screenshot.getTaskId() >= 0) { mAssistContentRequester.requestAssistContent( @@ -453,10 +443,6 @@ public class ScreenshotController implements ScreenshotHandler { (v, insets) -> WindowInsets.CONSUMED); } - private boolean shouldShowUi() { - return mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay; - } - void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) { withWindowAttached(() -> { if (screenshotPrivateProfileAccessibilityAnnouncementFix()) { @@ -491,9 +477,6 @@ public class ScreenshotController implements ScreenshotHandler { } mViewProxy.setPackageName(mPackageName); - - mViewProxy.updateOrientation( - mWindowManager.getCurrentWindowMetrics().getWindowInsets()); } /** @@ -542,7 +525,7 @@ public class ScreenshotController implements ScreenshotHandler { } mMessageContainerController.setView(mViewProxy.getView()); - mViewProxy.setCallbacks(new ScreenshotView.ScreenshotViewCallback() { + mViewProxy.setCallbacks(new ScreenshotShelfViewProxy.ScreenshotViewCallback() { @Override public void onUserInteraction() { if (DEBUG_INPUT) { @@ -552,13 +535,6 @@ public class ScreenshotController implements ScreenshotHandler { } @Override - public void onAction(Intent intent, UserHandle owner, boolean overrideTransition) { - Pair<ActivityOptions, ExitTransitionCoordinator> exit = createWindowTransition(); - mActionIntentExecutor.launchIntentAsync( - intent, owner, overrideTransition, exit.first, exit.second); - } - - @Override public void onDismiss() { finishDismiss(); } @@ -724,29 +700,40 @@ public class ScreenshotController implements ScreenshotHandler { * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on * failure). */ - private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) { + private void saveScreenshotAndToast(ScreenshotData screenshot, Consumer<Uri> finisher) { // Play the shutter sound to notify that we've taken a screenshot playCameraSoundIfNeeded(); - saveScreenshotInWorkerThread( - owner, - /* onComplete */ finisher, - /* actionsReadyListener */ imageData -> { - if (DEBUG_CALLBACK) { - Log.d(TAG, "returning URI to finisher (Consumer<URI>): " + imageData.uri); - } - finisher.accept(imageData.uri); - if (imageData.uri == null) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_save_text); - } else { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); - mScreenshotHandler.post(() -> Toast.makeText(mContext, - R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); - } - }, - null); + if (screenshotSaveImageExporter()) { + saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> { + if (result.uri != null) { + mScreenshotHandler.post(() -> Toast.makeText(mContext, + R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); + } + }); + } else { + saveScreenshotInWorkerThread( + screenshot.getUserHandle(), + /* onComplete */ finisher, + /* actionsReadyListener */ imageData -> { + if (DEBUG_CALLBACK) { + Log.d(TAG, + "returning URI to finisher (Consumer<URI>): " + imageData.uri); + } + finisher.accept(imageData.uri); + if (imageData.uri == null) { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, + mPackageName); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_save_text); + } else { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); + mScreenshotHandler.post(() -> Toast.makeText(mContext, + R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); + } + }, + null); + } } /** @@ -819,8 +806,8 @@ public class ScreenshotController implements ScreenshotHandler { mScreenshotHandler.cancelTimeout(); } - private void saveScreenshotInBackground( - ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher) { + private void saveScreenshotInBackground(ScreenshotData screenshot, UUID requestId, + Consumer<Uri> finisher, Consumer<ImageExporter.Result> onResult) { ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor, requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(), mDisplay.getDisplayId()); @@ -829,10 +816,7 @@ public class ScreenshotController implements ScreenshotHandler { ImageExporter.Result result = future.get(); Log.d(TAG, "Saved screenshot: " + result); logScreenshotResultStatus(result.uri, screenshot.getUserHandle()); - if (result.uri != null) { - mActionsController.setCompletedScreenshot(requestId, new ScreenshotSavedResult( - result.uri, screenshot.getUserOrDefault(), result.timestamp)); - } + onResult.accept(result); if (DEBUG_CALLBACK) { Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) " + "finisher.accept(\"" + result.uri + "\""); @@ -877,62 +861,6 @@ public class ScreenshotController implements ScreenshotHandler { mSaveInBgTask.execute(); } - - /** - * Sets up the action shade and its entrance animation, once we get the screenshot URI. - */ - private void showUiOnActionsReady(ScreenshotController.SavedImageData imageData) { - logSuccessOnActionsReady(imageData); - mScreenshotHandler.resetTimeout(); - - if (imageData.uri != null) { - if (DEBUG_UI) { - Log.d(TAG, "Showing UI actions"); - } - if (!imageData.owner.equals(Process.myUserHandle())) { - Log.d(TAG, "Screenshot saved to user " + imageData.owner + " as " - + imageData.uri); - } - mScreenshotHandler.post(() -> { - if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { - mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - mViewProxy.setChipIntents(imageData); - } - }); - } else { - mViewProxy.setChipIntents(imageData); - } - }); - } - } - - /** - * Sets up the action shade and its entrance animation, once we get the Quick Share action data. - */ - private void showUiOnQuickShareActionReady(ScreenshotController.QuickShareData quickShareData) { - if (DEBUG_UI) { - Log.d(TAG, "Showing UI for Quick Share action"); - } - if (quickShareData.quickShareAction != null) { - mScreenshotHandler.post(() -> { - if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { - mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - mViewProxy.addQuickShareChip(quickShareData.quickShareAction); - } - }); - } else { - mViewProxy.addQuickShareChip(quickShareData.quickShareAction); - } - }); - } - } - /** * Logs success/failure of the screenshot saving task, and shows an error if it failed. */ @@ -1028,9 +956,7 @@ public class ScreenshotController implements ScreenshotHandler { * Creates an instance of the controller for that specific display. * * @param display display to capture - * @param showUIOnExternalDisplay Whether the UI should be shown if this is an external - * display. */ - ScreenshotController create(Display display, boolean showUIOnExternalDisplay); + ScreenshotController create(Display display); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt index 1b5fa345ebb4..50215af30ad4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt @@ -18,7 +18,6 @@ package com.android.systemui.screenshot import android.animation.Animator import android.animation.AnimatorListenerAdapter -import android.app.Notification import android.content.Context import android.graphics.Bitmap import android.graphics.Rect @@ -45,7 +44,6 @@ import com.android.systemui.res.R import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW -import com.android.systemui.screenshot.ScreenshotController.SavedImageData import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER import com.android.systemui.screenshot.scroll.ScrollCaptureController import com.android.systemui.screenshot.ui.ScreenshotAnimationController @@ -70,13 +68,23 @@ constructor( private val thumbnailObserver: ThumbnailObserver, @Assisted private val context: Context, @Assisted private val displayId: Int -) : ScreenshotViewProxy { - override val view: ScreenshotShelfView = +) { + + interface ScreenshotViewCallback { + fun onUserInteraction() + + fun onDismiss() + + /** DOWN motion event was observed outside of the touchable areas of this view. */ + fun onTouchOutside() + } + + val view: ScreenshotShelfView = LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView - override val screenshotPreview: View - override var packageName: String = "" - override var callbacks: ScreenshotView.ScreenshotViewCallback? = null - override var screenshot: ScreenshotData? = null + val screenshotPreview: View + var packageName: String = "" + var callbacks: ScreenshotViewCallback? = null + var screenshot: ScreenshotData? = null set(value) { value?.let { viewModel.setScreenshotBitmap(it.bitmap) @@ -92,10 +100,11 @@ constructor( field = value } - override val isAttachedToWindow + val isAttachedToWindow get() = view.isAttachedToWindow - override var isDismissing = false - override var isPendingSharedTransition = false + + var isDismissing = false + var isPendingSharedTransition = false private val animationController = ScreenshotAnimationController(view, viewModel) private var inputMonitor: InputMonitorCompat? = null @@ -136,17 +145,17 @@ constructor( ) } - override fun reset() { + fun reset() { animationController.cancel() isPendingSharedTransition = false viewModel.reset() } - override fun updateInsets(insets: WindowInsets) { + + fun updateInsets(insets: WindowInsets) { view.updateInsets(insets) } - override fun updateOrientation(insets: WindowInsets) {} - override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator { + fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator { val entrance = animationController.getEntranceAnimation(screenRect, showFlash) { viewModel.setAnimationState(AnimationState.ENTRANCE_REVEAL) @@ -164,11 +173,7 @@ constructor( return entrance } - override fun addQuickShareChip(quickShareAction: Notification.Action) {} - - override fun setChipIntents(imageData: SavedImageData) {} - - override fun requestDismissal(event: ScreenshotEvent?) { + fun requestDismissal(event: ScreenshotEvent?) { requestDismissal(event, null) } @@ -187,6 +192,7 @@ constructor( override fun onAnimationStart(animator: Animator) { isDismissing = true } + override fun onAnimationEnd(animator: Animator) { isDismissing = false callbacks?.onDismiss() @@ -196,11 +202,7 @@ constructor( animator.start() } - override fun showScrollChip(packageName: String, onClick: Runnable) {} - - override fun hideScrollChip() {} - - override fun prepareScrollingTransition( + fun prepareScrollingTransition( response: ScrollCaptureResponse, screenBitmap: Bitmap, // unused newScreenshot: Bitmap, @@ -228,7 +230,7 @@ constructor( return r } - override fun startLongScreenshotTransition( + fun startLongScreenshotTransition( transitionDestination: Rect, onTransitionEnd: Runnable, longScreenshot: ScrollCaptureController.LongScreenshot, @@ -243,27 +245,27 @@ constructor( transitionAnimation.start() } - override fun restoreNonScrollingUi() { + fun restoreNonScrollingUi() { viewModel.setScrollableRect(null) viewModel.setScrollingScrimBitmap(null) animationController.restoreUI() callbacks?.onUserInteraction() // reset the timeout } - override fun stopInputListening() { + fun stopInputListening() { inputMonitor?.dispose() inputMonitor = null inputEventReceiver?.dispose() inputEventReceiver = null } - override fun requestFocus() { + fun requestFocus() { view.requestFocus() } - override fun announceForAccessibility(string: String) = view.announceForAccessibility(string) + fun announceForAccessibility(string: String) = view.announceForAccessibility(string) - override fun prepareEntranceAnimation(runnable: Runnable) { + fun prepareEntranceAnimation(runnable: Runnable) { view.viewTreeObserver.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { @@ -276,7 +278,7 @@ constructor( ) } - override fun fadeForSharedTransition() { + fun fadeForSharedTransition() { animationController.fadeForSharedTransition() } @@ -349,7 +351,7 @@ constructor( } @AssistedFactory - interface Factory : ScreenshotViewProxy.Factory { - override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy + interface Factory { + fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java deleted file mode 100644 index 59e38a836258..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ /dev/null @@ -1,1126 +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.screenshot; - -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; - -import static com.android.internal.jank.InteractionJankMonitor.CUJ_TAKE_SCREENSHOT; -import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; -import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS; -import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; -import static com.android.systemui.screenshot.LogConfig.DEBUG_SCROLL; -import static com.android.systemui.screenshot.LogConfig.DEBUG_UI; -import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW; -import static com.android.systemui.screenshot.LogConfig.logTag; -import static com.android.systemui.screenshot.ScreenshotController.SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS; - -import static java.util.Objects.requireNonNull; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ValueAnimator; -import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.BroadcastOptions; -import android.app.Notification; -import android.content.Context; -import android.content.Intent; -import android.content.res.ColorStateList; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BlendMode; -import android.graphics.Color; -import android.graphics.Insets; -import android.graphics.Matrix; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.Region; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.graphics.drawable.InsetDrawable; -import android.graphics.drawable.LayerDrawable; -import android.os.Bundle; -import android.os.Looper; -import android.os.RemoteException; -import android.os.UserHandle; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.MathUtils; -import android.view.Choreographer; -import android.view.Display; -import android.view.DisplayCutout; -import android.view.GestureDetector; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.ScrollCaptureResponse; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.view.WindowMetrics; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; -import android.widget.FrameLayout; -import android.widget.HorizontalScrollView; -import android.widget.ImageView; -import android.widget.LinearLayout; - -import androidx.constraintlayout.widget.ConstraintLayout; - -import com.android.internal.jank.InteractionJankMonitor; -import com.android.internal.logging.UiEventLogger; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.res.R; -import com.android.systemui.screenshot.scroll.ScrollCaptureController; -import com.android.systemui.shared.system.InputChannelCompat; -import com.android.systemui.shared.system.InputMonitorCompat; -import com.android.systemui.shared.system.QuickStepContract; - -import java.util.ArrayList; - -/** - * Handles the visual elements and animations for the screenshot flow. - */ -public class ScreenshotView extends FrameLayout implements - ViewTreeObserver.OnComputeInternalInsetsListener { - - public interface ScreenshotViewCallback { - void onUserInteraction(); - - void onAction(Intent intent, UserHandle owner, boolean overrideTransition); - - void onDismiss(); - - /** DOWN motion event was observed outside of the touchable areas of this view. */ - void onTouchOutside(); - } - - private static final String TAG = logTag(ScreenshotView.class); - - private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133; - private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217; - // delay before starting to fade in dismiss button - private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200; - private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234; - private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500; - private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234; - public static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; - private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; - private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; - - private final Resources mResources; - private final Interpolator mFastOutSlowIn; - private final DisplayMetrics mDisplayMetrics; - private final float mFixedSize; - private final AccessibilityManager mAccessibilityManager; - private final GestureDetector mSwipeDetector; - - private int mDefaultDisplay = Display.DEFAULT_DISPLAY; - private int mNavMode; - private boolean mOrientationPortrait; - private boolean mDirectionLTR; - - private ImageView mScrollingScrim; - private DraggableConstraintLayout mScreenshotStatic; - private ImageView mScreenshotPreview; - private ImageView mScreenshotBadge; - private View mScreenshotPreviewBorder; - private ImageView mScrollablePreview; - private ImageView mScreenshotFlash; - private ImageView mActionsContainerBackground; - private HorizontalScrollView mActionsContainer; - private LinearLayout mActionsView; - private FrameLayout mDismissButton; - private OverlayActionChip mShareChip; - private OverlayActionChip mEditChip; - private OverlayActionChip mScrollChip; - private OverlayActionChip mQuickShareChip; - - private UiEventLogger mUiEventLogger; - private ScreenshotViewCallback mCallbacks; - private boolean mPendingSharedTransition; - private InputMonitorCompat mInputMonitor; - private InputChannelCompat.InputEventReceiver mInputEventReceiver; - private boolean mShowScrollablePreview; - private String mPackageName = ""; - - private final ArrayList<OverlayActionChip> mSmartChips = new ArrayList<>(); - private PendingInteraction mPendingInteraction; - // Should only be set/used if the SCREENSHOT_METADATA flag is set. - private ScreenshotData mScreenshotData; - - private final InteractionJankMonitor mInteractionJankMonitor; - private FeatureFlags mFlags; - private final Bundle mInteractiveBroadcastOption; - - private enum PendingInteraction { - PREVIEW, - EDIT, - SHARE, - QUICK_SHARE - } - - public ScreenshotView(Context context) { - this(context, null); - } - - public ScreenshotView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ScreenshotView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public ScreenshotView( - Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - mResources = mContext.getResources(); - mInteractionJankMonitor = getInteractionJankMonitorInstance(); - - BroadcastOptions options = BroadcastOptions.makeBasic(); - options.setInteractive(true); - mInteractiveBroadcastOption = options.toBundle(); - - mFixedSize = mResources.getDimensionPixelSize(R.dimen.overlay_x_scale); - - // standard material ease - mFastOutSlowIn = - AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); - - mDisplayMetrics = new DisplayMetrics(); - mContext.getDisplay().getRealMetrics(mDisplayMetrics); - - mAccessibilityManager = AccessibilityManager.getInstance(mContext); - - mSwipeDetector = new GestureDetector(mContext, - new GestureDetector.SimpleOnGestureListener() { - final Rect mActionsRect = new Rect(); - - @Override - public boolean onScroll( - MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) { - mActionsContainer.getBoundsOnScreen(mActionsRect); - // return true if we aren't in the actions bar, or if we are but it isn't - // scrollable in the direction of movement - return !mActionsRect.contains((int) ev2.getRawX(), (int) ev2.getRawY()) - || !mActionsContainer.canScrollHorizontally((int) distanceX); - } - }); - mSwipeDetector.setIsLongpressEnabled(false); - addOnAttachStateChangeListener(new OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - startInputListening(); - } - - @Override - public void onViewDetachedFromWindow(View v) { - stopInputListening(); - } - }); - } - - private InteractionJankMonitor getInteractionJankMonitorInstance() { - return InteractionJankMonitor.getInstance(); - } - - public void hideScrollChip() { - mScrollChip.setVisibility(View.GONE); - } - - /** - * Called to display the scroll action chip when support is detected. - * - * @param packageName the owning package of the window to be captured - * @param onClick the action to take when the chip is clicked. - */ - public void showScrollChip(String packageName, Runnable onClick) { - if (DEBUG_SCROLL) { - Log.d(TAG, "Showing Scroll option"); - } - mScrollChip.setVisibility(VISIBLE); - mScrollChip.setOnClickListener((v) -> onClick.run()); - } - - @Override // ViewTreeObserver.OnComputeInternalInsetsListener - public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { - inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - inoutInfo.touchableRegion.set(getTouchRegion(true)); - } - - private Region getSwipeRegion() { - Region swipeRegion = new Region(); - - final Rect tmpRect = new Rect(); - int swipePadding = (int) FloatingWindowUtil.dpToPx( - mDisplayMetrics, DraggableConstraintLayout.SWIPE_PADDING_DP * -1); - mScreenshotPreview.getBoundsOnScreen(tmpRect); - tmpRect.inset(swipePadding, swipePadding); - swipeRegion.op(tmpRect, Region.Op.UNION); - mActionsContainerBackground.getBoundsOnScreen(tmpRect); - tmpRect.inset(swipePadding, swipePadding); - swipeRegion.op(tmpRect, Region.Op.UNION); - mDismissButton.getBoundsOnScreen(tmpRect); - swipeRegion.op(tmpRect, Region.Op.UNION); - - View messageContainer = findViewById(R.id.screenshot_message_container); - if (messageContainer != null) { - messageContainer.getBoundsOnScreen(tmpRect); - swipeRegion.op(tmpRect, Region.Op.UNION); - } - View messageDismiss = findViewById(R.id.message_dismiss_button); - if (messageDismiss != null) { - messageDismiss.getBoundsOnScreen(tmpRect); - swipeRegion.op(tmpRect, Region.Op.UNION); - } - - return swipeRegion; - } - - private Region getTouchRegion(boolean includeScrim) { - Region touchRegion = getSwipeRegion(); - - if (includeScrim && mScrollingScrim.getVisibility() == View.VISIBLE) { - final Rect tmpRect = new Rect(); - mScrollingScrim.getBoundsOnScreen(tmpRect); - touchRegion.op(tmpRect, Region.Op.UNION); - } - - if (QuickStepContract.isGesturalMode(mNavMode)) { - final WindowManager wm = mContext.getSystemService(WindowManager.class); - final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); - final Insets gestureInsets = windowMetrics.getWindowInsets().getInsets( - WindowInsets.Type.systemGestures()); - // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE - Rect inset = new Rect(0, 0, gestureInsets.left, mDisplayMetrics.heightPixels); - touchRegion.op(inset, Region.Op.UNION); - inset.set(mDisplayMetrics.widthPixels - gestureInsets.right, 0, - mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels); - touchRegion.op(inset, Region.Op.UNION); - } - return touchRegion; - } - - private void startInputListening() { - stopInputListening(); - mInputMonitor = new InputMonitorCompat("Screenshot", mDefaultDisplay); - mInputEventReceiver = mInputMonitor.getInputReceiver( - Looper.getMainLooper(), Choreographer.getInstance(), ev -> { - if (ev instanceof MotionEvent) { - MotionEvent event = (MotionEvent) ev; - if (event.getActionMasked() == MotionEvent.ACTION_DOWN - && !getTouchRegion(false).contains( - (int) event.getRawX(), (int) event.getRawY())) { - mCallbacks.onTouchOutside(); - } - } - }); - } - - void stopInputListening() { - if (mInputMonitor != null) { - mInputMonitor.dispose(); - mInputMonitor = null; - } - if (mInputEventReceiver != null) { - mInputEventReceiver.dispose(); - mInputEventReceiver = null; - } - } - - @Override // View - protected void onFinishInflate() { - super.onFinishInflate(); - mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim)); - mScreenshotStatic = requireNonNull(findViewById(R.id.screenshot_static)); - mScreenshotPreview = requireNonNull(findViewById(R.id.screenshot_preview)); - - mScreenshotPreviewBorder = requireNonNull( - findViewById(R.id.screenshot_preview_border)); - mScreenshotPreview.setClipToOutline(true); - mScreenshotBadge = requireNonNull(findViewById(R.id.screenshot_badge)); - - mActionsContainerBackground = requireNonNull(findViewById( - R.id.actions_container_background)); - mActionsContainer = requireNonNull(findViewById(R.id.actions_container)); - mActionsView = requireNonNull(findViewById(R.id.screenshot_actions)); - mDismissButton = requireNonNull(findViewById(R.id.screenshot_dismiss_button)); - mScrollablePreview = requireNonNull(findViewById(R.id.screenshot_scrollable_preview)); - mScreenshotFlash = requireNonNull(findViewById(R.id.screenshot_flash)); - mShareChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_share_chip)); - mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip)); - mScrollChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_scroll_chip)); - - setFocusable(true); - mActionsContainer.setScrollX(0); - - mNavMode = getResources().getInteger( - com.android.internal.R.integer.config_navBarInteractionMode); - mOrientationPortrait = - getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; - mDirectionLTR = - getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; - - // Get focus so that the key events go to the layout. - setFocusableInTouchMode(true); - requestFocus(); - - mScreenshotStatic.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() { - @Override - public void onInteraction() { - mCallbacks.onUserInteraction(); - } - - @Override - public void onSwipeDismissInitiated(Animator animator) { - if (DEBUG_DISMISS) { - Log.d(ScreenshotView.TAG, "dismiss triggered via swipe gesture"); - } - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, 0, - mPackageName); - } - - @Override - public void onDismissComplete() { - if (mInteractionJankMonitor.isInstrumenting(CUJ_TAKE_SCREENSHOT)) { - mInteractionJankMonitor.end(CUJ_TAKE_SCREENSHOT); - } - mCallbacks.onDismiss(); - } - }); - } - - View getScreenshotPreview() { - return mScreenshotPreview; - } - - void setUiEventLogger(UiEventLogger uiEventLogger) { - mUiEventLogger = uiEventLogger; - } - - void setCallbacks(ScreenshotViewCallback callbacks) { - mCallbacks = callbacks; - } - - void setFlags(FeatureFlags flags) { - mFlags = flags; - } - - void setScreenshot(Bitmap bitmap, Insets screenInsets) { - mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets)); - } - - void setScreenshot(ScreenshotData screenshot) { - mScreenshotData = screenshot; - setScreenshot(screenshot.getBitmap(), screenshot.getInsets()); - mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, screenshot.getBitmap(), - screenshot.getInsets())); - } - - void setPackageName(String packageName) { - mPackageName = packageName; - } - - void setDefaultDisplay(int displayId) { - mDefaultDisplay = displayId; - } - - void updateInsets(WindowInsets insets) { - int orientation = mContext.getResources().getConfiguration().orientation; - mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT); - FrameLayout.LayoutParams p = - (FrameLayout.LayoutParams) mScreenshotStatic.getLayoutParams(); - DisplayCutout cutout = insets.getDisplayCutout(); - Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); - if (cutout == null) { - p.setMargins(0, 0, 0, navBarInsets.bottom); - } else { - Insets waterfall = cutout.getWaterfallInsets(); - if (mOrientationPortrait) { - p.setMargins( - waterfall.left, - Math.max(cutout.getSafeInsetTop(), waterfall.top), - waterfall.right, - Math.max(cutout.getSafeInsetBottom(), - Math.max(navBarInsets.bottom, waterfall.bottom))); - } else { - p.setMargins( - Math.max(cutout.getSafeInsetLeft(), waterfall.left), - waterfall.top, - Math.max(cutout.getSafeInsetRight(), waterfall.right), - Math.max(navBarInsets.bottom, waterfall.bottom)); - } - } - mScreenshotStatic.setLayoutParams(p); - mScreenshotStatic.requestLayout(); - } - - void updateOrientation(WindowInsets insets) { - int orientation = mContext.getResources().getConfiguration().orientation; - mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT); - updateInsets(insets); - ViewGroup.LayoutParams params = mScreenshotPreview.getLayoutParams(); - if (mOrientationPortrait) { - params.width = (int) mFixedSize; - params.height = LayoutParams.WRAP_CONTENT; - mScreenshotPreview.setScaleType(ImageView.ScaleType.FIT_START); - } else { - params.width = LayoutParams.WRAP_CONTENT; - params.height = (int) mFixedSize; - mScreenshotPreview.setScaleType(ImageView.ScaleType.FIT_END); - } - - mScreenshotPreview.setLayoutParams(params); - } - - AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash) { - if (DEBUG_ANIM) { - Log.d(TAG, "createAnim: bounds=" + bounds + " showFlash=" + showFlash); - } - - Rect targetPosition = new Rect(); - mScreenshotPreview.getHitRect(targetPosition); - - // ratio of preview width, end vs. start size - float cornerScale = - mFixedSize / (mOrientationPortrait ? bounds.width() : bounds.height()); - final float currentScale = 1 / cornerScale; - - AnimatorSet dropInAnimation = new AnimatorSet(); - ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1); - flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS); - flashInAnimator.setInterpolator(mFastOutSlowIn); - flashInAnimator.addUpdateListener(animation -> - mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); - - ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0); - flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS); - flashOutAnimator.setInterpolator(mFastOutSlowIn); - flashOutAnimator.addUpdateListener(animation -> - mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); - - // animate from the current location, to the static preview location - final PointF startPos = new PointF(bounds.centerX(), bounds.centerY()); - final PointF finalPos = new PointF(targetPosition.exactCenterX(), - targetPosition.exactCenterY()); - - // Shift to screen coordinates so that the animation runs on top of the entire screen, - // including e.g. bars covering the display cutout. - int[] locInScreen = mScreenshotPreview.getLocationOnScreen(); - startPos.offset(targetPosition.left - locInScreen[0], targetPosition.top - locInScreen[1]); - - if (DEBUG_ANIM) { - Log.d(TAG, "toCorner: startPos=" + startPos); - Log.d(TAG, "toCorner: finalPos=" + finalPos); - } - - ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1); - toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS); - - toCorner.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - mScreenshotPreview.setScaleX(currentScale); - mScreenshotPreview.setScaleY(currentScale); - mScreenshotPreview.setVisibility(View.VISIBLE); - if (mAccessibilityManager.isEnabled()) { - mDismissButton.setAlpha(0); - mDismissButton.setVisibility(View.VISIBLE); - } - } - }); - - float xPositionPct = - SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; - float dismissPct = - SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; - float scalePct = - SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; - toCorner.addUpdateListener(animation -> { - float t = animation.getAnimatedFraction(); - if (t < scalePct) { - float scale = MathUtils.lerp( - currentScale, 1, mFastOutSlowIn.getInterpolation(t / scalePct)); - mScreenshotPreview.setScaleX(scale); - mScreenshotPreview.setScaleY(scale); - } else { - mScreenshotPreview.setScaleX(1); - mScreenshotPreview.setScaleY(1); - } - - if (t < xPositionPct) { - float xCenter = MathUtils.lerp(startPos.x, finalPos.x, - mFastOutSlowIn.getInterpolation(t / xPositionPct)); - mScreenshotPreview.setX(xCenter - mScreenshotPreview.getWidth() / 2f); - } else { - mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f); - } - float yCenter = MathUtils.lerp( - startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t)); - mScreenshotPreview.setY(yCenter - mScreenshotPreview.getHeight() / 2f); - - if (t >= dismissPct) { - mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct)); - float currentX = mScreenshotPreview.getX(); - float currentY = mScreenshotPreview.getY(); - mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f); - if (mDirectionLTR) { - mDismissButton.setX(currentX + mScreenshotPreview.getWidth() - - mDismissButton.getWidth() / 2f); - } else { - mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f); - } - } - }); - - mScreenshotFlash.setAlpha(0f); - mScreenshotFlash.setVisibility(View.VISIBLE); - - ValueAnimator borderFadeIn = ValueAnimator.ofFloat(0, 1); - borderFadeIn.setDuration(100); - borderFadeIn.addUpdateListener((animation) -> { - float borderAlpha = animation.getAnimatedFraction(); - mScreenshotPreviewBorder.setAlpha(borderAlpha); - mScreenshotBadge.setAlpha(borderAlpha); - }); - - if (showFlash) { - dropInAnimation.play(flashOutAnimator).after(flashInAnimator); - dropInAnimation.play(flashOutAnimator).with(toCorner); - } else { - dropInAnimation.play(toCorner); - } - dropInAnimation.play(borderFadeIn).after(toCorner); - - dropInAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - mInteractionJankMonitor.cancel(CUJ_TAKE_SCREENSHOT); - } - - @Override - public void onAnimationStart(Animator animation) { - InteractionJankMonitor.Configuration.Builder builder = - InteractionJankMonitor.Configuration.Builder.withView( - CUJ_TAKE_SCREENSHOT, mScreenshotPreview) - .setTag("DropIn"); - mInteractionJankMonitor.begin(builder); - } - - @Override - public void onAnimationEnd(Animator animation) { - if (DEBUG_ANIM) { - Log.d(TAG, "drop-in animation ended"); - } - mDismissButton.setOnClickListener(view -> { - if (DEBUG_INPUT) { - Log.d(TAG, "dismiss button clicked"); - } - mUiEventLogger.log( - ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL, 0, mPackageName); - animateDismissal(); - }); - mDismissButton.setAlpha(1); - float dismissOffset = mDismissButton.getWidth() / 2f; - float finalDismissX = mDirectionLTR - ? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f - : finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f; - mDismissButton.setX(finalDismissX); - mDismissButton.setY( - finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f); - mScreenshotPreview.setScaleX(1); - mScreenshotPreview.setScaleY(1); - mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f); - mScreenshotPreview.setY(finalPos.y - mScreenshotPreview.getHeight() / 2f); - requestLayout(); - mInteractionJankMonitor.end(CUJ_TAKE_SCREENSHOT); - createScreenshotActionsShadeAnimation().start(); - } - }); - - return dropInAnimation; - } - - ValueAnimator createScreenshotActionsShadeAnimation() { - // By default the activities won't be able to start immediately; override this to keep - // the same behavior as if started from a notification - try { - ActivityManager.getService().resumeAppSwitches(); - } catch (RemoteException e) { - } - - ArrayList<OverlayActionChip> chips = new ArrayList<>(); - - mShareChip.setContentDescription(mContext.getString(R.string.screenshot_share_description)); - mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); - mShareChip.setOnClickListener(v -> { - mShareChip.setIsPending(true); - mEditChip.setIsPending(false); - if (mQuickShareChip != null) { - mQuickShareChip.setIsPending(false); - } - mPendingInteraction = PendingInteraction.SHARE; - }); - chips.add(mShareChip); - - mEditChip.setContentDescription( - mContext.getString(R.string.screenshot_edit_description)); - mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), - true); - mEditChip.setOnClickListener(v -> { - mEditChip.setIsPending(true); - mShareChip.setIsPending(false); - if (mQuickShareChip != null) { - mQuickShareChip.setIsPending(false); - } - mPendingInteraction = PendingInteraction.EDIT; - }); - chips.add(mEditChip); - - mScreenshotPreview.setOnClickListener(v -> { - mShareChip.setIsPending(false); - mEditChip.setIsPending(false); - if (mQuickShareChip != null) { - mQuickShareChip.setIsPending(false); - } - mPendingInteraction = PendingInteraction.PREVIEW; - }); - - mScrollChip.setText(mContext.getString(R.string.screenshot_scroll_label)); - mScrollChip.setIcon(Icon.createWithResource(mContext, - R.drawable.ic_screenshot_scroll), true); - chips.add(mScrollChip); - - // remove the margin from the last chip so that it's correctly aligned with the end - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) - mActionsView.getChildAt(0).getLayoutParams(); - params.setMarginEnd(0); - mActionsView.getChildAt(0).setLayoutParams(params); - - ValueAnimator animator = ValueAnimator.ofFloat(0, 1); - animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS); - float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS - / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS; - mActionsContainer.setAlpha(0f); - mActionsContainerBackground.setAlpha(0f); - mActionsContainer.setVisibility(View.VISIBLE); - mActionsContainerBackground.setVisibility(View.VISIBLE); - - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - mInteractionJankMonitor.cancel(CUJ_TAKE_SCREENSHOT); - } - - @Override - public void onAnimationEnd(Animator animation) { - mInteractionJankMonitor.end(CUJ_TAKE_SCREENSHOT); - } - - @Override - public void onAnimationStart(Animator animation) { - InteractionJankMonitor.Configuration.Builder builder = - InteractionJankMonitor.Configuration.Builder.withView( - CUJ_TAKE_SCREENSHOT, mScreenshotStatic) - .setTag("Actions") - .setTimeout(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS); - mInteractionJankMonitor.begin(builder); - } - }); - - animator.addUpdateListener(animation -> { - float t = animation.getAnimatedFraction(); - float containerAlpha = t < alphaFraction ? t / alphaFraction : 1; - mActionsContainer.setAlpha(containerAlpha); - mActionsContainerBackground.setAlpha(containerAlpha); - float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X - + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X)); - mActionsContainer.setScaleX(containerScale); - mActionsContainerBackground.setScaleX(containerScale); - for (OverlayActionChip chip : chips) { - chip.setAlpha(t); - chip.setScaleX(1 / containerScale); // invert to keep size of children constant - } - mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); - mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); - mActionsContainerBackground.setPivotX( - mDirectionLTR ? 0 : mActionsContainerBackground.getWidth()); - }); - return animator; - } - - void badgeScreenshot(@Nullable Drawable badge) { - mScreenshotBadge.setImageDrawable(badge); - mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE); - } - - void setChipIntents(ScreenshotController.SavedImageData imageData) { - mShareChip.setOnClickListener(v -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName); - prepareSharedTransition(); - - Intent shareIntent = ActionIntentCreator.INSTANCE.createShareWithSubject( - imageData.uri, imageData.subject); - mCallbacks.onAction(shareIntent, imageData.owner, false); - - }); - mEditChip.setOnClickListener(v -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName); - prepareSharedTransition(); - mCallbacks.onAction( - ActionIntentCreator.INSTANCE.createEdit(imageData.uri, mContext), - imageData.owner, true); - }); - mScreenshotPreview.setOnClickListener(v -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName); - prepareSharedTransition(); - mCallbacks.onAction( - ActionIntentCreator.INSTANCE.createEdit(imageData.uri, mContext), - imageData.owner, true); - }); - if (mQuickShareChip != null) { - if (imageData.quickShareAction != null) { - mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent, - () -> { - mUiEventLogger.log( - ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED, 0, - mPackageName); - animateDismissal(); - }); - } else { - // hide chip and unset pending interaction if necessary, since we don't actually - // have a useable quick share intent - Log.wtf(TAG, "Showed quick share chip, but quick share intent was null"); - if (mPendingInteraction == PendingInteraction.QUICK_SHARE) { - mPendingInteraction = null; - } - mQuickShareChip.setVisibility(GONE); - } - } - - if (mPendingInteraction != null) { - switch (mPendingInteraction) { - case PREVIEW: - mScreenshotPreview.callOnClick(); - break; - case SHARE: - mShareChip.callOnClick(); - break; - case EDIT: - mEditChip.callOnClick(); - break; - case QUICK_SHARE: - mQuickShareChip.callOnClick(); - break; - } - } else { - LayoutInflater inflater = LayoutInflater.from(mContext); - - for (Notification.Action smartAction : imageData.smartActions) { - OverlayActionChip actionChip = (OverlayActionChip) inflater.inflate( - R.layout.overlay_action_chip, mActionsView, false); - actionChip.setText(smartAction.title); - actionChip.setIcon(smartAction.getIcon(), false); - actionChip.setPendingIntent(smartAction.actionIntent, - () -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED, - 0, mPackageName); - animateDismissal(); - }); - actionChip.setAlpha(1); - mActionsView.addView(actionChip, mActionsView.getChildCount() - 1); - mSmartChips.add(actionChip); - } - } - } - - void addQuickShareChip(Notification.Action quickShareAction) { - if (mQuickShareChip != null) { - mSmartChips.remove(mQuickShareChip); - mActionsView.removeView(mQuickShareChip); - } - if (mPendingInteraction == PendingInteraction.QUICK_SHARE) { - mPendingInteraction = null; - } - if (mPendingInteraction == null) { - LayoutInflater inflater = LayoutInflater.from(mContext); - mQuickShareChip = (OverlayActionChip) inflater.inflate( - R.layout.overlay_action_chip, mActionsView, false); - mQuickShareChip.setText(quickShareAction.title); - mQuickShareChip.setIcon(quickShareAction.getIcon(), false); - mQuickShareChip.setOnClickListener(v -> { - mShareChip.setIsPending(false); - mEditChip.setIsPending(false); - mQuickShareChip.setIsPending(true); - mPendingInteraction = PendingInteraction.QUICK_SHARE; - }); - mQuickShareChip.setAlpha(1); - mActionsView.addView(mQuickShareChip); - mSmartChips.add(mQuickShareChip); - } - } - - private Rect scrollableAreaOnScreen(ScrollCaptureResponse response) { - Rect r = new Rect(response.getBoundsInWindow()); - Rect windowInScreen = response.getWindowBounds(); - r.offset(windowInScreen.left, windowInScreen.top); - r.intersect(new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)); - return r; - } - - void startLongScreenshotTransition(Rect destination, Runnable onTransitionEnd, - ScrollCaptureController.LongScreenshot longScreenshot) { - mPendingSharedTransition = true; - AnimatorSet animSet = new AnimatorSet(); - - ValueAnimator scrimAnim = ValueAnimator.ofFloat(0, 1); - scrimAnim.addUpdateListener(animation -> - mScrollingScrim.setAlpha(1 - animation.getAnimatedFraction())); - - if (mShowScrollablePreview) { - mScrollablePreview.setImageBitmap(longScreenshot.toBitmap()); - float startX = mScrollablePreview.getX(); - float startY = mScrollablePreview.getY(); - int[] locInScreen = mScrollablePreview.getLocationOnScreen(); - destination.offset((int) startX - locInScreen[0], (int) startY - locInScreen[1]); - mScrollablePreview.setPivotX(0); - mScrollablePreview.setPivotY(0); - mScrollablePreview.setAlpha(1f); - float currentScale = mScrollablePreview.getWidth() / (float) longScreenshot.getWidth(); - Matrix matrix = new Matrix(); - matrix.setScale(currentScale, currentScale); - matrix.postTranslate( - longScreenshot.getLeft() * currentScale, - longScreenshot.getTop() * currentScale); - mScrollablePreview.setImageMatrix(matrix); - float destinationScale = destination.width() / (float) mScrollablePreview.getWidth(); - - ValueAnimator previewAnim = ValueAnimator.ofFloat(0, 1); - previewAnim.addUpdateListener(animation -> { - float t = animation.getAnimatedFraction(); - float currScale = MathUtils.lerp(1, destinationScale, t); - mScrollablePreview.setScaleX(currScale); - mScrollablePreview.setScaleY(currScale); - mScrollablePreview.setX(MathUtils.lerp(startX, destination.left, t)); - mScrollablePreview.setY(MathUtils.lerp(startY, destination.top, t)); - }); - ValueAnimator previewFadeAnim = ValueAnimator.ofFloat(1, 0); - previewFadeAnim.addUpdateListener(animation -> - mScrollablePreview.setAlpha(1 - animation.getAnimatedFraction())); - animSet.play(previewAnim).with(scrimAnim).before(previewFadeAnim); - previewAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - onTransitionEnd.run(); - } - }); - } else { - // if we switched orientations between the original screenshot and the long screenshot - // capture, just fade out the scrim instead of running the preview animation - animSet.play(scrimAnim); - animSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - onTransitionEnd.run(); - } - }); - } - animSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - mCallbacks.onDismiss(); - } - }); - animSet.start(); - } - - void prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap, - Bitmap newBitmap, boolean screenshotTakenInPortrait) { - mShowScrollablePreview = (screenshotTakenInPortrait == mOrientationPortrait); - - mScrollingScrim.setImageBitmap(newBitmap); - mScrollingScrim.setVisibility(View.VISIBLE); - - if (mShowScrollablePreview) { - Rect scrollableArea = scrollableAreaOnScreen(response); - - float scale = mFixedSize - / (mOrientationPortrait ? screenBitmap.getWidth() : screenBitmap.getHeight()); - ConstraintLayout.LayoutParams params = - (ConstraintLayout.LayoutParams) mScrollablePreview.getLayoutParams(); - - params.width = (int) (scale * scrollableArea.width()); - params.height = (int) (scale * scrollableArea.height()); - Matrix matrix = new Matrix(); - matrix.setScale(scale, scale); - matrix.postTranslate(-scrollableArea.left * scale, -scrollableArea.top * scale); - - mScrollablePreview.setTranslationX(scale - * (mDirectionLTR ? scrollableArea.left : scrollableArea.right - getWidth())); - mScrollablePreview.setTranslationY(scale * scrollableArea.top); - mScrollablePreview.setImageMatrix(matrix); - mScrollablePreview.setImageBitmap(screenBitmap); - mScrollablePreview.setVisibility(View.VISIBLE); - } - mDismissButton.setVisibility(View.GONE); - mActionsContainer.setVisibility(View.GONE); - // set these invisible, but not gone, so that the views are laid out correctly - mActionsContainerBackground.setVisibility(View.INVISIBLE); - mScreenshotPreviewBorder.setVisibility(View.INVISIBLE); - mScreenshotPreview.setVisibility(View.INVISIBLE); - mScrollingScrim.setImageTintBlendMode(BlendMode.SRC_ATOP); - ValueAnimator anim = ValueAnimator.ofFloat(0, .3f); - anim.addUpdateListener(animation -> mScrollingScrim.setImageTintList( - ColorStateList.valueOf(Color.argb((float) animation.getAnimatedValue(), 0, 0, 0)))); - anim.setDuration(200); - anim.start(); - } - - void restoreNonScrollingUi() { - mScrollChip.setVisibility(View.GONE); - mScrollablePreview.setVisibility(View.GONE); - mScrollingScrim.setVisibility(View.GONE); - - if (mAccessibilityManager.isEnabled()) { - mDismissButton.setVisibility(View.VISIBLE); - } - mActionsContainer.setVisibility(View.VISIBLE); - mActionsContainerBackground.setVisibility(View.VISIBLE); - mScreenshotPreviewBorder.setVisibility(View.VISIBLE); - mScreenshotPreview.setVisibility(View.VISIBLE); - // reset the timeout - mCallbacks.onUserInteraction(); - } - - boolean isDismissing() { - return mScreenshotStatic.isDismissing(); - } - - boolean isPendingSharedTransition() { - return mPendingSharedTransition; - } - - void animateDismissal() { - mScreenshotStatic.dismiss(); - } - - void reset() { - if (DEBUG_UI) { - Log.d(TAG, "reset screenshot view"); - } - mScreenshotStatic.cancelDismissal(); - if (DEBUG_WINDOW) { - Log.d(TAG, "removing OnComputeInternalInsetsListener"); - } - // Make sure we clean up the view tree observer - getViewTreeObserver().removeOnComputeInternalInsetsListener(this); - // Clear any references to the bitmap - mScreenshotPreview.setImageDrawable(null); - mScreenshotPreview.setVisibility(View.INVISIBLE); - mScreenshotPreview.setAlpha(1f); - mScreenshotPreviewBorder.setAlpha(0); - mScreenshotBadge.setAlpha(0f); - mScreenshotBadge.setVisibility(View.GONE); - mScreenshotBadge.setImageDrawable(null); - mPendingSharedTransition = false; - mActionsContainerBackground.setVisibility(View.INVISIBLE); - mActionsContainer.setVisibility(View.GONE); - mDismissButton.setVisibility(View.GONE); - mScrollingScrim.setVisibility(View.GONE); - mScrollablePreview.setVisibility(View.GONE); - mScreenshotStatic.setTranslationX(0); - mScreenshotPreview.setContentDescription( - mContext.getResources().getString(R.string.screenshot_preview_description)); - mScreenshotPreview.setOnClickListener(null); - mShareChip.setOnClickListener(null); - mScrollingScrim.setVisibility(View.GONE); - mEditChip.setOnClickListener(null); - mShareChip.setIsPending(false); - mEditChip.setIsPending(false); - mPendingInteraction = null; - for (OverlayActionChip chip : mSmartChips) { - mActionsView.removeView(chip); - } - mSmartChips.clear(); - mQuickShareChip = null; - setAlpha(1); - mScreenshotStatic.setAlpha(1); - mScreenshotData = null; - } - - private void prepareSharedTransition() { - mPendingSharedTransition = true; - // fade out non-preview UI - createScreenshotFadeDismissAnimation().start(); - } - - ValueAnimator createScreenshotFadeDismissAnimation() { - ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); - alphaAnim.addUpdateListener(animation -> { - float alpha = 1 - animation.getAnimatedFraction(); - mDismissButton.setAlpha(alpha); - mActionsContainerBackground.setAlpha(alpha); - mActionsContainer.setAlpha(alpha); - mScreenshotPreviewBorder.setAlpha(alpha); - mScreenshotBadge.setAlpha(alpha); - }); - alphaAnim.setDuration(600); - return alphaAnim; - } - - /** - * Create a drawable using the size of the bitmap and insets as the fractional inset parameters. - */ - private static Drawable createScreenDrawable(Resources res, Bitmap bitmap, Insets insets) { - int insettedWidth = bitmap.getWidth() - insets.left - insets.right; - int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom; - - BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bitmap); - if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 - || bitmap.getHeight() == 0) { - Log.e(TAG, "Can't create inset drawable, using 0 insets bitmap and insets create " - + "degenerate region: " + bitmap.getWidth() + "x" + bitmap.getHeight() + " " - + bitmapDrawable); - return bitmapDrawable; - } - - InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable, - -1f * insets.left / insettedWidth, - -1f * insets.top / insettedHeight, - -1f * insets.right / insettedWidth, - -1f * insets.bottom / insettedHeight); - - if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) { - // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need - // to fill in the background of the drawable. - return new LayerDrawable(new Drawable[]{ - new ColorDrawable(Color.BLACK), insetDrawable}); - } else { - return insetDrawable; - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt deleted file mode 100644 index df93a5e56c22..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.screenshot - -import android.animation.Animator -import android.app.Notification -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Rect -import android.view.ScrollCaptureResponse -import android.view.View -import android.view.ViewGroup -import android.view.WindowInsets -import com.android.systemui.screenshot.scroll.ScrollCaptureController - -/** Abstraction of the surface between ScreenshotController and ScreenshotView */ -interface ScreenshotViewProxy { - val view: ViewGroup - val screenshotPreview: View - - var packageName: String - var callbacks: ScreenshotView.ScreenshotViewCallback? - var screenshot: ScreenshotData? - - val isAttachedToWindow: Boolean - val isDismissing: Boolean - val isPendingSharedTransition: Boolean - - fun reset() - fun updateInsets(insets: WindowInsets) - fun updateOrientation(insets: WindowInsets) - fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator - fun addQuickShareChip(quickShareAction: Notification.Action) - fun setChipIntents(imageData: ScreenshotController.SavedImageData) - fun requestDismissal(event: ScreenshotEvent?) - - fun showScrollChip(packageName: String, onClick: Runnable) - fun hideScrollChip() - fun prepareScrollingTransition( - response: ScrollCaptureResponse, - screenBitmap: Bitmap, - newScreenshot: Bitmap, - screenshotTakenInPortrait: Boolean, - onTransitionPrepared: Runnable, - ) - fun startLongScreenshotTransition( - transitionDestination: Rect, - onTransitionEnd: Runnable, - longScreenshot: ScrollCaptureController.LongScreenshot - ) - fun restoreNonScrollingUi() - fun fadeForSharedTransition() - - fun stopInputListening() - fun requestFocus() - fun announceForAccessibility(string: String) - fun prepareEntranceAnimation(runnable: Runnable) - - interface Factory { - fun getProxy(context: Context, displayId: Int): ScreenshotViewProxy - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt index 2699657847b9..07f6e85cfad8 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt @@ -219,7 +219,7 @@ constructor( } private fun getScreenshotController(display: Display): ScreenshotController { - val controller = screenshotController ?: screenshotControllerFactory.create(display, false) + val controller = screenshotController ?: screenshotControllerFactory.create(display) screenshotController = controller return controller } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java index 8c833eccb1fb..bd9e295b58f8 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java @@ -33,6 +33,7 @@ import android.content.ClipData; import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.HardwareRenderer; import android.graphics.RecordingCanvas; @@ -267,6 +268,8 @@ final class AppClipsViewModel extends ViewModel { } private boolean canAppStartThroughLauncher(String packageName) { + // Use Intent.resolveActivity API to check if the intent resolves as that is what Android + // uses internally when apps use Context.startActivity. return getMainLauncherIntentForPackage(packageName).resolveActivity(mPackageManager) != null; } @@ -366,10 +369,19 @@ final class AppClipsViewModel extends ViewModel { return taskInfo.topActivityInfo.loadLabel(mPackageManager).toString(); } - private Intent getMainLauncherIntentForPackage(String packageName) { - return new Intent(ACTION_MAIN) - .addCategory(CATEGORY_LAUNCHER) - .setPackage(packageName); + private Intent getMainLauncherIntentForPackage(String pkgName) { + Intent intent = new Intent(ACTION_MAIN).addCategory(CATEGORY_LAUNCHER).setPackage(pkgName); + + // Not all apps use DEFAULT_CATEGORY for their main launcher activity so the exact component + // needs to be queried and set on the Intent in order for note-taking apps to be able to + // start this intent. When starting an activity with an implicit intent, Android adds the + // DEFAULT_CATEGORY flag otherwise it fails to resolve the intent. + ResolveInfo resolvedActivity = mPackageManager.resolveActivity(intent, /* flags= */ 0); + if (resolvedActivity != null) { + intent.setComponent(resolvedActivity.getComponentInfo().getComponentName()); + } + + return intent; } /** Helper factory to help with injecting {@link AppClipsViewModel}. */ diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index 56ba1af4e83b..682f848bfaad 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -24,12 +24,10 @@ import com.android.systemui.screenshot.ImageCapture; import com.android.systemui.screenshot.ImageCaptureImpl; import com.android.systemui.screenshot.ScreenshotPolicy; import com.android.systemui.screenshot.ScreenshotPolicyImpl; -import com.android.systemui.screenshot.ScreenshotShelfViewProxy; import com.android.systemui.screenshot.ScreenshotSoundController; import com.android.systemui.screenshot.ScreenshotSoundControllerImpl; import com.android.systemui.screenshot.ScreenshotSoundProvider; import com.android.systemui.screenshot.ScreenshotSoundProviderImpl; -import com.android.systemui.screenshot.ScreenshotViewProxy; import com.android.systemui.screenshot.TakeScreenshotExecutor; import com.android.systemui.screenshot.TakeScreenshotExecutorImpl; import com.android.systemui.screenshot.TakeScreenshotService; @@ -92,8 +90,4 @@ public abstract class ScreenshotModule { AccessibilityManager accessibilityManager) { return new ScreenshotViewModel(accessibilityManager); } - - @Binds - abstract ScreenshotViewProxy.Factory bindScreenshotViewProxyFactory( - ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 04de2c220a6b..c1caeed05cb6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1774,8 +1774,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // the small clock here // With migrateClocksToBlueprint, weather clock will have behaviors similar to other clocks if (!MigrateClocksToBlueprint.isEnabled()) { + boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled(); if (mKeyguardStatusViewController.isLargeClockBlockingNotificationShelf() - && hasVisibleNotifications() && isOnAod()) { + && hasVisibleNotifications() && (isOnAod() || bypassEnabled)) { return SMALL; } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt index 4d43ad52019a..a4fed873362b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -19,8 +19,6 @@ import android.content.Context import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R -import com.android.systemui.shade.shared.flag.DualShade -import com.android.systemui.shade.shared.model.ShadeMode import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -105,18 +103,26 @@ interface ShadeRepository { @Deprecated("Use ShadeInteractor.isQsBypassingShade instead") val legacyExpandImmediate: StateFlow<Boolean> - val shadeMode: StateFlow<ShadeMode> - /** Whether dual shade should be aligned to the bottom (true) or to the top (false). */ val isDualShadeAlignedToBottom: Boolean + /** + * Whether the shade layout should be wide (true) or narrow (false). + * + * In a wide layout, notifications and quick settings each take up only half the screen width + * (whether they are shown at the same time or not). In a narrow layout, they can each be as + * wide as the entire screen. + */ + val isShadeLayoutWide: StateFlow<Boolean> + /** True when QS is taking up the entire screen, i.e. fully expanded on a non-unfolded phone. */ @Deprecated("Use ShadeInteractor instead") val legacyQsFullscreen: StateFlow<Boolean> /** NPVC.mClosing as a flow. */ @Deprecated("Use ShadeAnimationInteractor instead") val legacyIsClosing: StateFlow<Boolean> - fun setShadeMode(mode: ShadeMode) + /** Sets whether the shade layout should be wide (true) or narrow (false). */ + fun setShadeLayoutWide(isShadeLayoutWide: Boolean) /** Sets whether a closing animation is happening. */ @Deprecated("Use ShadeAnimationInteractor instead") fun setLegacyIsClosing(isClosing: Boolean) @@ -183,6 +189,7 @@ interface ShadeRepository { class ShadeRepositoryImpl @Inject constructor(@Application applicationContext: Context) : ShadeRepository { private val _qsExpansion = MutableStateFlow(0f) + @Deprecated("Use ShadeInteractor.qsExpansion instead") override val qsExpansion: StateFlow<Float> = _qsExpansion.asStateFlow() private val _lockscreenShadeExpansion = MutableStateFlow(0f) @@ -204,6 +211,7 @@ class ShadeRepositoryImpl @Inject constructor(@Application applicationContext: C @Deprecated("Use ShadeInteractor instead") override val legacyShadeTracking: StateFlow<Boolean> = _legacyShadeTracking.asStateFlow() + @Deprecated("Use ShadeInteractor.isUserInteractingWithShade instead") override val legacyLockscreenShadeTracking = MutableStateFlow(false) private val _legacyQsTracking = MutableStateFlow(false) @@ -227,20 +235,22 @@ class ShadeRepositoryImpl @Inject constructor(@Application applicationContext: C @Deprecated("Use ShadeInteractor instead") override val legacyQsFullscreen: StateFlow<Boolean> = _legacyQsFullscreen.asStateFlow() - val _shadeMode = MutableStateFlow(if (DualShade.isEnabled) ShadeMode.Dual else ShadeMode.Single) - override val shadeMode: StateFlow<ShadeMode> = _shadeMode.asStateFlow() + private val _isShadeLayoutWide = MutableStateFlow(false) + override val isShadeLayoutWide: StateFlow<Boolean> = _isShadeLayoutWide.asStateFlow() override val isDualShadeAlignedToBottom = applicationContext.resources.getBoolean(R.bool.config_dualShadeAlignedToBottom) - override fun setShadeMode(shadeMode: ShadeMode) { - _shadeMode.value = shadeMode + override fun setShadeLayoutWide(isShadeLayoutWide: Boolean) { + _isShadeLayoutWide.value = isShadeLayoutWide } + @Deprecated("Use ShadeInteractor instead") override fun setLegacyQsFullscreen(legacyQsFullscreen: Boolean) { _legacyQsFullscreen.value = legacyQsFullscreen } + @Deprecated("Use ShadeInteractor instead") override fun setLegacyExpandImmediate(legacyExpandImmediate: Boolean) { _legacyExpandImmediate.value = legacyExpandImmediate } 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 ef0a842ecc0c..c1f8a0be646d 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 @@ -52,9 +52,21 @@ interface ShadeInteractor : BaseShadeInteractor { /** Are touches allowed on the notification panel? */ val isShadeTouchable: Flow<Boolean> - /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */ + /** Whether the shade can be expanded from QQS to QS. */ val isExpandToQsEnabled: Flow<Boolean> + /** The version of the shade layout to use. */ + val shadeMode: StateFlow<ShadeMode> + + /** + * Whether the shade layout should be wide (true) or narrow (false). + * + * In a wide layout, notifications and quick settings each take up only half the screen width + * (whether they are shown at the same time or not). In a narrow layout, they can each be as + * wide as the entire screen. + */ + val isShadeLayoutWide: StateFlow<Boolean> + /** How to align the shade content. */ val shadeAlignment: ShadeAlignment } @@ -113,8 +125,6 @@ interface BaseShadeInteractor { * animating. */ val isUserInteractingWithQs: Flow<Boolean> - - val shadeMode: StateFlow<ShadeMode> } fun createAnyExpansionFlow( 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 6226d077b135..e77aca93aeca 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 @@ -47,5 +47,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override val isShadeTouchable: Flow<Boolean> = inactiveFlowBoolean override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single) + override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean override val shadeAlignment: ShadeAlignment = ShadeAlignment.Top } 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 55f019b01164..684a4845144e 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 @@ -25,7 +25,9 @@ import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.shared.model.ShadeAlignment +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository @@ -60,8 +62,7 @@ constructor( ) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor { override val isShadeEnabled: StateFlow<Boolean> = disableFlagsRepository.disableFlags - .map { isDisabledByFlags -> isDisabledByFlags.isShadeEnabled() } - .distinctUntilChanged() + .map { it.isShadeEnabled() } .stateIn(scope, SharingStarted.Eagerly, initialValue = false) override val isQsEnabled: StateFlow<Boolean> = @@ -102,6 +103,17 @@ constructor( } } + override val isShadeLayoutWide: StateFlow<Boolean> = shadeRepository.isShadeLayoutWide + + override val shadeMode: StateFlow<ShadeMode> = + isShadeLayoutWide + .map(this::determineShadeMode) + .stateIn( + scope, + SharingStarted.Eagerly, + initialValue = determineShadeMode(isShadeLayoutWide.value) + ) + override val shadeAlignment: ShadeAlignment = if (shadeRepository.isDualShadeAlignedToBottom) { ShadeAlignment.Bottom @@ -125,4 +137,12 @@ constructor( disableFlags.isQuickSettingsEnabled() && !isDozing } + + private fun determineShadeMode(isShadeLayoutWide: Boolean): ShadeMode { + return when { + DualShade.isEnabled -> ShadeMode.Dual + isShadeLayoutWide -> ShadeMode.Split + else -> ShadeMode.Single + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt index 7d46d2ba17da..f39ee9afd039 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt @@ -21,7 +21,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.shade.data.repository.ShadeRepository -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -105,8 +104,6 @@ constructor( override val isUserInteractingWithQs: Flow<Boolean> = userInteractingFlow(repository.legacyQsTracking, repository.qsExpansion) - override val shadeMode: StateFlow<ShadeMode> = repository.shadeMode - /** * Return a flow for whether a user is interacting with an expandable shade component using * tracking and expansion flows. NOTE: expansion must be a `StateFlow` to guarantee that diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt index d5b4f4d7e623..b2142a515923 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt @@ -22,8 +22,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.shade.data.repository.ShadeRepository -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject @@ -45,10 +43,7 @@ constructor( @Application scope: CoroutineScope, sceneInteractor: SceneInteractor, sharedNotificationContainerInteractor: SharedNotificationContainerInteractor, - shadeRepository: ShadeRepository, ) : BaseShadeInteractor { - override val shadeMode: StateFlow<ShadeMode> = shadeRepository.shadeMode - override val shadeExpansion: StateFlow<Float> = sceneBasedExpansion(sceneInteractor, SceneFamilies.NotifShade) .stateIn(scope, SharingStarted.Eagerly, 0f) diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt index 354d379d2ea2..5eb3a1cc1348 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt @@ -29,8 +29,6 @@ import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.shade.TouchLogger.Companion.logTouchesTo import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor -import com.android.systemui.shade.shared.flag.DualShade -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shade.transition.ScrimShadeTransitionController import com.android.systemui.statusbar.PulseExpansionHandler import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -40,7 +38,6 @@ import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @@ -63,7 +60,7 @@ constructor( ) : CoreStartable { override fun start() { - hydrateShadeMode() + hydrateShadeLayoutWidth() hydrateShadeExpansionStateManager() logTouchesTo(touchLog) scrimShadeTransitionController.init() @@ -86,22 +83,17 @@ constructor( } } - private fun hydrateShadeMode() { - if (DualShade.isEnabled) { - shadeRepository.setShadeMode(ShadeMode.Dual) - return - } + private fun hydrateShadeLayoutWidth() { applicationScope.launch { configurationRepository.onAnyConfigurationChange // Force initial collection. .onStart { emit(Unit) } - .map { applicationContext.resources } - .map { resources -> - splitShadeStateController.shouldUseSplitNotificationShade(resources) - } - .collect { isSplitShade -> - shadeRepository.setShadeMode( - if (isSplitShade) ShadeMode.Split else ShadeMode.Single + .collect { + val resources = applicationContext.resources + // The configuration for 'shouldUseSplitNotificationShade' dictates the width of + // the shade in both split-shade and dual-shade modes. + shadeRepository.setShadeLayoutWide( + splitShadeStateController.shouldUseSplitNotificationShade(resources) ) } } diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt index 2f58b35ae182..ea4e065be84b 100644 --- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt @@ -18,51 +18,35 @@ package com.android.systemui.smartspace.dagger import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.smartspace.SmartspacePrecondition import com.android.systemui.smartspace.SmartspaceTargetFilter -import com.android.systemui.smartspace.data.repository.SmartspaceRepositoryModule import com.android.systemui.smartspace.preconditions.LockscreenPrecondition import dagger.Binds import dagger.BindsOptionalOf import dagger.Module import javax.inject.Named -@Module(subcomponents = [SmartspaceViewComponent::class], - includes = [SmartspaceRepositoryModule::class]) +@Module(subcomponents = [SmartspaceViewComponent::class]) abstract class SmartspaceModule { @Module companion object { - /** - * The BcSmartspaceDataProvider for dreams. - */ + /** The BcSmartspaceDataProvider for dreams. */ const val DREAM_SMARTSPACE_DATA_PLUGIN = "dreams_smartspace_data_plugin" - /** - * The BcSmartspaceDataPlugin for the standalone weather on dream. - */ + /** The BcSmartspaceDataPlugin for the standalone weather on dream. */ const val DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN = "dream_weather_smartspace_data_plugin" - /** - * The target filter for smartspace over lockscreen. - */ + /** The target filter for smartspace over lockscreen. */ const val LOCKSCREEN_SMARTSPACE_TARGET_FILTER = "lockscreen_smartspace_target_filter" - /** - * The precondition for smartspace over lockscreen - */ + /** The precondition for smartspace over lockscreen */ const val LOCKSCREEN_SMARTSPACE_PRECONDITION = "lockscreen_smartspace_precondition" - /** - * The BcSmartspaceDataPlugin for the standalone date (+alarm+dnd). - */ + /** The BcSmartspaceDataPlugin for the standalone date (+alarm+dnd). */ const val DATE_SMARTSPACE_DATA_PLUGIN = "date_smartspace_data_plugin" - /** - * The BcSmartspaceDataPlugin for the standalone weather. - */ + /** The BcSmartspaceDataPlugin for the standalone weather. */ const val WEATHER_SMARTSPACE_DATA_PLUGIN = "weather_smartspace_data_plugin" - /** - * The BcSmartspaceDataProvider for the glanceable hub. - */ + /** The BcSmartspaceDataProvider for the glanceable hub. */ const val GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN = "glanceable_hub_smartspace_data_plugin" } diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt deleted file mode 100644 index 52a1c1555591..000000000000 --- a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt +++ /dev/null @@ -1,77 +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.smartspace.data.repository - -import android.app.smartspace.SmartspaceTarget -import android.os.Parcelable -import android.widget.RemoteViews -import com.android.systemui.communal.smartspace.CommunalSmartspaceController -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.plugins.BcSmartspaceDataPlugin -import java.util.concurrent.Executor -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart - -interface SmartspaceRepository { - /** Whether [RemoteViews] are passed through smartspace targets. */ - val isSmartspaceRemoteViewsEnabled: Boolean - - /** Smartspace targets for the communal surface. */ - val communalSmartspaceTargets: Flow<List<SmartspaceTarget>> -} - -@SysUISingleton -class SmartspaceRepositoryImpl -@Inject -constructor( - private val communalSmartspaceController: CommunalSmartspaceController, - @Main private val uiExecutor: Executor, -) : SmartspaceRepository, BcSmartspaceDataPlugin.SmartspaceTargetListener { - - override val isSmartspaceRemoteViewsEnabled: Boolean - get() = android.app.smartspace.flags.Flags.remoteViews() - - private val _communalSmartspaceTargets: MutableStateFlow<List<SmartspaceTarget>> = - MutableStateFlow(emptyList()) - override val communalSmartspaceTargets: Flow<List<SmartspaceTarget>> = - _communalSmartspaceTargets - .onStart { - uiExecutor.execute { - communalSmartspaceController.addListener( - listener = this@SmartspaceRepositoryImpl - ) - } - } - .onCompletion { - uiExecutor.execute { - communalSmartspaceController.removeListener( - listener = this@SmartspaceRepositoryImpl - ) - } - } - - override fun onSmartspaceTargetsUpdated(targetsNullable: MutableList<out Parcelable>?) { - targetsNullable?.let { targets -> - _communalSmartspaceTargets.value = targets.filterIsInstance<SmartspaceTarget>() - } - ?: run { _communalSmartspaceTargets.value = emptyList() } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index e505ef753dce..0957e5a5df35 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -51,6 +51,7 @@ import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus; import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.res.R; import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor; @@ -223,6 +224,10 @@ public class StatusBarStateControllerImpl implements mSceneContainerOcclusionInteractorLazy.get().getInvisibleDueToOcclusion(), this::calculateStateFromSceneFramework), this::onStatusBarStateChanged); + + mJavaAdapter.alwaysCollectFlow( + mKeyguardTransitionInteractorLazy.get().transitionValue(KeyguardState.AOD), + this::onAodKeyguardStateTransitionValueChanged); } } @@ -693,6 +698,14 @@ public class StatusBarStateControllerImpl implements updateStateAndNotifyListeners(newState); } + private void onAodKeyguardStateTransitionValueChanged(float value) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + return; + } + + setDozeAmountInternal(value); + } + private static final Map<SceneKey, Integer> sStatusBarStateByLockedSceneKey = Map.of( Scenes.Lockscreen, StatusBarState.KEYGUARD, Scenes.Bouncer, StatusBarState.KEYGUARD, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt index cac3f252a7e4..bafec38efe9d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt @@ -51,33 +51,34 @@ class EndCastScreenToOtherDeviceDialogDelegate( } private fun getMessage(): String { + val hostDeviceName = state.projectionState.hostDeviceName return if (state.projectionState is MediaProjectionState.Projecting.SingleTask) { val appBeingSharedName = endMediaProjectionDialogHelper.getAppName(state.projectionState) - if (appBeingSharedName != null && state.deviceName != null) { + if (appBeingSharedName != null && hostDeviceName != null) { context.getString( R.string.cast_to_other_device_stop_dialog_message_specific_app_with_device, appBeingSharedName, - state.deviceName, + hostDeviceName, ) } else if (appBeingSharedName != null) { context.getString( R.string.cast_to_other_device_stop_dialog_message_specific_app, appBeingSharedName, ) - } else if (state.deviceName != null) { + } else if (hostDeviceName != null) { context.getString( R.string.cast_to_other_device_stop_dialog_message_generic_with_device, - state.deviceName + hostDeviceName, ) } else { context.getString(R.string.cast_to_other_device_stop_dialog_message_generic) } } else { - if (state.deviceName != null) { + if (hostDeviceName != null) { context.getString( R.string.cast_to_other_device_stop_dialog_message_entire_screen_with_device, - state.deviceName + hostDeviceName, ) } else { context.getString( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt index 4183cdd4369d..f5e17df71ff9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.content.Context import androidx.annotation.DrawableRes -import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton @@ -58,7 +57,6 @@ constructor( private val mediaProjectionChipInteractor: MediaProjectionChipInteractor, private val mediaRouterChipInteractor: MediaRouterChipInteractor, private val systemClock: SystemClock, - private val dialogTransitionAnimator: DialogTransitionAnimator, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, ) : OngoingActivityChipViewModel { /** @@ -175,7 +173,6 @@ constructor( startTimeMs = systemClock.elapsedRealtime(), createDialogLaunchOnClickListener( createCastScreenToOtherDeviceDialogDelegate(state), - dialogTransitionAnimator, ), ) } @@ -191,7 +188,6 @@ constructor( colors = ColorsModel.Red, createDialogLaunchOnClickListener( createGenericCastToOtherDeviceDialogDelegate(deviceName), - dialogTransitionAnimator, ), ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt index ce60fab3ea6b..191c221f43c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt @@ -71,11 +71,11 @@ constructor( { str1 = type.name str2 = state.hostPackage + str3 = state.hostDeviceName }, - { "State: Projecting(type=$str1 hostPackage=$str2)" } + { "State: Projecting(type=$str1 hostPackage=$str2 hostDevice=$str3)" } ) - // TODO(b/351851835): Get the device name. - ProjectionChipModel.Projecting(type, state, deviceName = null) + ProjectionChipModel.Projecting(type, state) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt index a1a5e82e808e..85682f5eb8ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt @@ -26,16 +26,10 @@ sealed class ProjectionChipModel { /** There is no media being projected. */ data object NotProjecting : ProjectionChipModel() - /** - * Media is currently being projected. - * - * @property deviceName the name of the device receiving the projection, or null if the - * projection is to this device (as opposed to a different device). - */ + /** Media is currently being projected. */ data class Projecting( val type: Type, val projectionState: MediaProjectionState.Projecting, - val deviceName: String?, ) : ProjectionChipModel() enum class Type { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt index df25d57315ed..e201652242a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.app.ActivityManager import android.content.Context import androidx.annotation.DrawableRes -import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton @@ -52,7 +51,6 @@ constructor( private val interactor: ScreenRecordChipInteractor, private val systemClock: SystemClock, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, - private val dialogTransitionAnimator: DialogTransitionAnimator, ) : OngoingActivityChipViewModel { override val chip: StateFlow<OngoingActivityChipModel> = interactor.screenRecordState @@ -78,7 +76,6 @@ constructor( startTimeMs = systemClock.elapsedRealtime(), createDialogLaunchOnClickListener( createDelegate(state.recordedTask), - dialogTransitionAnimator, ), ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt index c09772068093..45260e18e1ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel import android.content.Context import androidx.annotation.DrawableRes -import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton @@ -52,7 +51,6 @@ constructor( private val context: Context, private val mediaProjectionChipInteractor: MediaProjectionChipInteractor, private val systemClock: SystemClock, - private val dialogTransitionAnimator: DialogTransitionAnimator, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, ) : OngoingActivityChipViewModel { override val chip: StateFlow<OngoingActivityChipModel> = @@ -89,10 +87,7 @@ constructor( colors = ColorsModel.Red, // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time. startTimeMs = systemClock.elapsedRealtime(), - createDialogLaunchOnClickListener( - createShareToAppDialogDelegate(state), - dialogTransitionAnimator - ), + createDialogLaunchOnClickListener(createShareToAppDialogDelegate(state)), ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt index 0dbf5d613038..65f94ac756cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt @@ -17,10 +17,7 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import android.view.View -import com.android.systemui.animation.DialogTransitionAnimator -import com.android.systemui.res.R import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel -import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.phone.SystemUIDialog import kotlinx.coroutines.flow.StateFlow @@ -36,19 +33,10 @@ interface OngoingActivityChipViewModel { /** Creates a chip click listener that launches a dialog created by [dialogDelegate]. */ fun createDialogLaunchOnClickListener( dialogDelegate: SystemUIDialog.Delegate, - dialogTransitionAnimator: DialogTransitionAnimator, ): View.OnClickListener { return View.OnClickListener { view -> val dialog = dialogDelegate.createDialog() - val launchableView = - view.requireViewById<ChipBackgroundContainer>( - R.id.ongoing_activity_chip_background - ) - // TODO(b/343699052): This makes a beautiful animate-in, but the - // animate-out looks odd because the dialog animates back into the chip - // but then the chip disappears. If we aren't able to address - // b/343699052 in time for launch, we should just use `dialog.show`. - dialogTransitionAnimator.showFromView(dialog, launchableView) + dialog.show() } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index f98a88f5328b..e48c28d3f3ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -698,7 +698,7 @@ public final class NotificationEntry extends ListEntry implements NotificationRo } public void setHeadsUpIsVisible() { - if (row != null) row.setHeadsUpIsVisible(); + if (row != null) row.markHeadsUpSeen(); } //TODO: i'm imagining a world where this isn't just the row, but I could be rwong diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt index f166d32812ae..5adf31b75fa7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt @@ -13,18 +13,12 @@ class VisualStabilityProvider @Inject constructor() { /** The subset of active listeners which are temporary (will be removed after called) */ private val temporaryListeners = ArraySet<OnReorderingAllowedListener>() - private val banListeners = ListenerSet<OnReorderingBannedListener>() - var isReorderingAllowed = true set(value) { if (field != value) { field = value if (value) { notifyReorderingAllowed() - } else { - banListeners.forEach { listener -> - listener.onReorderingBanned() - } } } } @@ -44,10 +38,6 @@ class VisualStabilityProvider @Inject constructor() { allListeners.addIfAbsent(listener) } - fun addPersistentReorderingBannedListener(listener: OnReorderingBannedListener) { - banListeners.addIfAbsent(listener) - } - /** Add a listener which will be removed when it is called. */ fun addTemporaryReorderingAllowedListener(listener: OnReorderingAllowedListener) { // Only add to the temporary set if it was added to the global set @@ -67,8 +57,3 @@ class VisualStabilityProvider @Inject constructor() { fun interface OnReorderingAllowedListener { fun onReorderingAllowed() } - -fun interface OnReorderingBannedListener { - fun onReorderingBanned() -} - 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 eebbb13005b9..f22f39937820 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 @@ -25,6 +25,7 @@ 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 +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @@ -50,48 +51,65 @@ constructor( val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow /** Set of currently pinned top-level heads up rows to be displayed. */ - val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> = - headsUpRepository.activeHeadsUpRows.flatMapLatest { repositories -> - if (repositories.isNotEmpty()) { - val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> = - repositories.map { repo -> repo.isPinned.map { isPinned -> repo to isPinned } } - combine(toCombine) { pairs -> - pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet() + val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(emptySet()) + } else { + headsUpRepository.activeHeadsUpRows.flatMapLatest { repositories -> + if (repositories.isNotEmpty()) { + val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> = + repositories.map { repo -> + repo.isPinned.map { isPinned -> repo to isPinned } + } + combine(toCombine) { pairs -> + pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet() + } + } else { + // if the set is empty, there are no flows to combine + flowOf(emptySet()) } - } else { - // if the set is empty, there are no flows to combine - flowOf(emptySet()) } } + } /** Are there any pinned heads up rows to display? */ - val hasPinnedRows: Flow<Boolean> = - headsUpRepository.activeHeadsUpRows.flatMapLatest { rows -> - if (rows.isNotEmpty()) { - combine(rows.map { it.isPinned }) { pins -> pins.any { it } } - } else { - // if the set is empty, there are no flows to combine - flowOf(false) + val hasPinnedRows: Flow<Boolean> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + headsUpRepository.activeHeadsUpRows.flatMapLatest { rows -> + if (rows.isNotEmpty()) { + combine(rows.map { it.isPinned }) { pins -> pins.any { it } } + } else { + // if the set is empty, there are no flows to combine + flowOf(false) + } } } + } - val isHeadsUpOrAnimatingAway: Flow<Boolean> = - combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) { - hasPinnedRows, - animatingAway -> - hasPinnedRows || animatingAway - } - .debounce { isHeadsUpOrAnimatingAway -> - if (isHeadsUpOrAnimatingAway) { - 0 - } else { - // When the last pinned entry is removed from the [HeadsUpRepository], - // there might be a delay before the View starts animating. - 50L + val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) { + hasPinnedRows, + animatingAway -> + hasPinnedRows || animatingAway } - } - .onStart { emit(false) } // emit false, so we don't wait for the initial update - .distinctUntilChanged() + .debounce { isHeadsUpOrAnimatingAway -> + if (isHeadsUpOrAnimatingAway) { + 0 + } else { + // When the last pinned entry is removed from the [HeadsUpRepository], + // there might be a delay before the View starts animating. + 50L + } + } + .onStart { emit(false) } // emit false, so we don't wait for the initial update + .distinctUntilChanged() + } + } private val canShowHeadsUp: Flow<Boolean> = combine( @@ -109,10 +127,15 @@ constructor( } } - val showHeadsUpStatusBar: Flow<Boolean> = - combine(hasPinnedRows, canShowHeadsUp) { hasPinnedRows, canShowHeadsUp -> - hasPinnedRows && canShowHeadsUp + val showHeadsUpStatusBar: Flow<Boolean> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + combine(hasPinnedRows, canShowHeadsUp) { hasPinnedRows, canShowHeadsUp -> + hasPinnedRows && canShowHeadsUp + } } + } fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor = HeadsUpRowInteractor(key as HeadsUpRowRepository) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 582d847d5011..1cbb16e3983a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -859,8 +859,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } @Override - public void setHeadsUpIsVisible() { - super.setHeadsUpIsVisible(); + public void markHeadsUpSeen() { + super.markHeadsUpSeen(); mMustStayOnScreen = false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 2af119f98f4a..6becbd295264 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -637,7 +637,10 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro return false; } - public void setHeadsUpIsVisible() { + /** + * Called, when the notification has been seen by the user in the heads up state. + */ + public void markHeadsUpSeen() { } public boolean showingPulsing() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt index b5ea861c19a6..b8af3698fb63 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row import android.app.Notification +import android.app.Notification.RichOngoingStyle import android.app.PendingIntent import android.content.Context import android.util.Log @@ -68,12 +69,14 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : builder: Notification.Builder, systemUIContext: Context, packageContext: Context - ): RichOngoingContentModel? = + ): RichOngoingContentModel? { + if (builder.style !is RichOngoingStyle) return null + try { val sbn = entry.sbn val notification = sbn.notification val icon = IconModel(notification.smallIcon) - if (sbn.packageName == "com.google.android.deskclock") { + return if (sbn.packageName == "com.google.android.deskclock") { when (notification.channelId) { "Timers v2" -> { parseTimerNotification(notification, icon) @@ -90,8 +93,9 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : } else null } catch (e: Exception) { Log.e("RONs", "Error parsing RON", e) - null + return null } + } /** * FOR PROTOTYPING ONLY: create a RON TimerContentModel using the time information available diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java index d1e5ab04a630..83de2265b98e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java @@ -176,7 +176,7 @@ public class ExpandableViewState extends ViewState { expandableView.setInShelf(inShelf); if (headsUpIsVisible) { - expandableView.setHeadsUpIsVisible(); + expandableView.markHeadsUpSeen(); } } } @@ -231,7 +231,7 @@ public class ExpandableViewState extends ViewState { expandableView.setInShelf(this.inShelf); if (headsUpIsVisible) { - expandableView.setHeadsUpIsVisible(); + expandableView.markHeadsUpSeen(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 132d0f045f5f..1789ad6a7216 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -6462,7 +6462,6 @@ public class NotificationStackScrollLayout }; public HeadsUpTouchHelper.Callback getHeadsUpCallback() { - SceneContainerFlag.assertInLegacyMode(); return mHeadsUpCallback; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 497ffcab32f2..6881d11bc95f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -38,12 +38,14 @@ import com.android.systemui.util.kotlin.DisposableHandles import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch /** Binds the shared notification container to its view-model. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class SharedNotificationContainerBinder @Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt index 52cb48be041f..8d73983e4053 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt @@ -51,8 +51,8 @@ constructor(private val viewModel: NotificationListViewModel) { } removed.forEach { key -> val row = obtainView(key) - parentView.generateHeadsUpAnimation(row, /* isHeadsUp = */ false) - row.setHeadsUpIsVisible() + parentView.generateHeadsUpAnimation(row, /* isHeadsUp= */ false) + row.markHeadsUpSeen() } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java index 91b5d0be6f04..a5388564d5fd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java @@ -414,6 +414,14 @@ public class AutoTileManager implements UserAwareController { } } + @Override + public void onFeatureEnabledChanged(boolean enabled) { + if (!enabled) { + mHost.removeTile(BRIGHTNESS); + mHandler.post(() -> mReduceBrightColorsController.removeCallback(this)); + } + } + private void addReduceBrightColorsTile() { if (mAutoTracker.isAdded(BRIGHTNESS)) return; mHost.addTile(BRIGHTNESS); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java index bd0097e8fc3f..398c1d43d4fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DarkIconDispatcherImpl.java @@ -74,9 +74,9 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher, mLightModeIconColorSingleTone = Color.WHITE; } else { mDarkModeIconColorSingleTone = context.getColor( - com.android.settingslib.R.color.black); + com.android.settingslib.R.color.dark_mode_icon_color_single_tone); mLightModeIconColorSingleTone = context.getColor( - com.android.settingslib.R.color.white); + com.android.settingslib.R.color.light_mode_icon_color_single_tone); } mTransitionsController = lightBarTransitionsControllerFactory.create(this); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index a11cbc3bf231..98869bef5bf0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -37,6 +37,7 @@ import androidx.annotation.VisibleForTesting; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.Dumpable; +import com.android.systemui.Flags; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -54,6 +55,7 @@ import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.tuner.TunerService; import com.android.systemui.unfold.FoldAodAnimationController; import com.android.systemui.unfold.SysUIUnfoldComponent; +import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; import java.util.Optional; @@ -86,6 +88,7 @@ public class DozeParameters implements private final FoldAodAnimationController mFoldAodAnimationController; private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; private final UserTracker mUserTracker; + private final SecureSettings mSecureSettings; private boolean mDozeAlwaysOn; private boolean mControlScreenOffAnimation; @@ -130,7 +133,8 @@ public class DozeParameters implements ConfigurationController configurationController, StatusBarStateController statusBarStateController, UserTracker userTracker, - DozeInteractor dozeInteractor) { + DozeInteractor dozeInteractor, + SecureSettings secureSettings) { mResources = resources; mAmbientDisplayConfiguration = ambientDisplayConfiguration; mAlwaysOnPolicy = alwaysOnDisplayPolicy; @@ -144,6 +148,7 @@ public class DozeParameters implements mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; mUserTracker = userTracker; mDozeInteractor = dozeInteractor; + mSecureSettings = secureSettings; keyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback); tunerService.addTunable( @@ -160,7 +165,8 @@ public class DozeParameters implements mFoldAodAnimationController.addCallback(this); } - SettingsObserver quickPickupSettingsObserver = new SettingsObserver(context, handler); + SettingsObserver quickPickupSettingsObserver = + new SettingsObserver(context, handler, mSecureSettings); quickPickupSettingsObserver.observe(); batteryController.addCallback(new BatteryStateChangeCallback() { @@ -479,18 +485,36 @@ public class DozeParameters implements Settings.Secure.getUriFor(Settings.Secure.DOZE_ALWAYS_ON); private final Context mContext; - SettingsObserver(Context context, Handler handler) { + private final Handler mHandler; + private final SecureSettings mSecureSettings; + + SettingsObserver(Context context, Handler handler, SecureSettings secureSettings) { super(handler); mContext = context; + mHandler = handler; + mSecureSettings = secureSettings; } void observe() { - ContentResolver resolver = mContext.getContentResolver(); - resolver.registerContentObserver(mQuickPickupGesture, false, this, - UserHandle.USER_ALL); - resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL); - resolver.registerContentObserver(mAlwaysOnEnabled, false, this, UserHandle.USER_ALL); - update(null); + if (Flags.registerContentObserversAsync()) { + mSecureSettings.registerContentObserverForUserAsync(mQuickPickupGesture, + this, UserHandle.USER_ALL); + mSecureSettings.registerContentObserverForUserAsync(mPickupGesture, + this, UserHandle.USER_ALL); + mSecureSettings.registerContentObserverForUserAsync(mAlwaysOnEnabled, + this, UserHandle.USER_ALL, + // The register calls are called in order, so this ensures that update() + // is called after them all and value retrieval isn't racy. + () -> mHandler.post(() -> update(null))); + } else { + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(mQuickPickupGesture, false, this, + UserHandle.USER_ALL); + resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(mAlwaysOnEnabled, false, this, + UserHandle.USER_ALL); + update(null); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index 0623bb2c274a..a2e44dffb767 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -31,7 +31,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.SystemBarUtils; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; @@ -40,7 +39,6 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; -import com.android.systemui.statusbar.notification.collection.provider.OnReorderingBannedListener; import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; @@ -88,7 +86,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>(); private final VisualStabilityProvider mVisualStabilityProvider; - private AvalancheController mAvalancheController; + private final AvalancheController mAvalancheController; // TODO(b/328393698) move the topHeadsUpRow logic to an interactor private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow = @@ -173,11 +171,8 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements updateResources(); } }); - if (!NotificationsHeadsUpRefactor.isEnabled()) { - javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(), + javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(), this::onShadeOrQsExpanded); - } - mVisualStabilityProvider.addPersistentReorderingBannedListener(mOnReorderingBannedListener); } public void setAnimationStateHandler(AnimationStateHandler handler) { @@ -272,10 +267,9 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements } private void onShadeOrQsExpanded(Boolean isExpanded) { - NotificationsHeadsUpRefactor.assertInLegacyMode(); if (isExpanded != mIsExpanded) { mIsExpanded = isExpanded; - if (isExpanded) { + if (!NotificationsHeadsUpRefactor.isEnabled() && isExpanded) { mHeadsUpAnimatingAway.setValue(false); } } @@ -385,8 +379,6 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> { mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); - mAvalancheController.setEnableAtRuntime(true); - for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) { if (isHeadsUpEntry(entry.getKey())) { // Maybe the heads-up was removed already @@ -397,29 +389,6 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); }; - private final OnReorderingBannedListener mOnReorderingBannedListener = () -> { - if (mAvalancheController != null) { - // Waiting HUNs in AvalancheController are still promoted to the HUN section and thus - // seen in open shade; clear them so we don't show them again when the shade closes and - // reordering is allowed again. - final int numDropped = mAvalancheController.getWaitingKeys().size(); - mAvalancheController.logDroppedHunsInBackground(numDropped); - mAvalancheController.clearNext(); - - // In open shade the first HUN is pinned, and visual stability logic prevents us from - // unpinning this first HUN as long as the shade remains open. AvalancheController only - // shows the next HUN when the currently showing HUN is unpinned, so we must disable - // throttling here so that the incoming HUN stream is not forever paused. This is reset - // when reorder becomes allowed. - mAvalancheController.setEnableAtRuntime(false); - - // Note that we cannot do the above when - // 1) the remove runnable runs because its delay means it may not run before shade close - // 2) reordering is allowed again (when shade closes) because the HUN appear animation - // will have started by then - } - }; - /////////////////////////////////////////////////////////////////////////////////////////////// // HeadsUpManager utility (protected) methods overrides: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index d0a62e77539f..84e601848b91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -455,8 +455,8 @@ public class KeyguardStatusBarView extends RelativeLayout { float luminance = Color.luminance(textColor); @ColorInt int iconColor = Utils.getColorStateListDefaultColor(mContext, luminance < 0.5 - ? com.android.settingslib.R.color.black - : com.android.settingslib.R.color.white); + ? com.android.settingslib.R.color.dark_mode_icon_color_single_tone + : com.android.settingslib.R.color.light_mode_icon_color_single_tone); @ColorInt int contrastColor = luminance < 0.5 ? DarkIconDispatcherImpl.DEFAULT_ICON_TINT : DarkIconDispatcherImpl.DEFAULT_INVERSE_ICON_TINT; @@ -467,7 +467,7 @@ public class KeyguardStatusBarView extends RelativeLayout { if (userSwitcherName != null) { userSwitcherName.setTextColor(Utils.getColorStateListDefaultColor( mContext, - com.android.settingslib.R.color.white)); + com.android.settingslib.R.color.light_mode_icon_color_single_tone)); } if (iconManager != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt index 824415eaea05..231a8c65a246 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt @@ -39,7 +39,7 @@ data class LetterboxAppearance( ) { override fun toString(): String { val appearanceString = - ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance) + ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance) return "LetterboxAppearance{$appearanceString, $appearanceRegions}" } } @@ -57,16 +57,14 @@ constructor( private val letterboxBackgroundProvider: LetterboxBackgroundProvider, ) : Dumpable { - private val darkAppearanceIconColor = - context.getColor( - // For a dark background status bar, use a *light* icon color. - com.android.settingslib.R.color.white - ) - private val lightAppearanceIconColor = - context.getColor( - // For a light background status bar, use a *dark* icon color. - com.android.settingslib.R.color.black - ) + private val darkAppearanceIconColor = context.getColor( + // For a dark background status bar, use a *light* icon color. + com.android.settingslib.R.color.light_mode_icon_color_single_tone + ) + private val lightAppearanceIconColor = context.getColor( + // For a light background status bar, use a *dark* icon color. + com.android.settingslib.R.color.dark_mode_icon_color_single_tone + ) init { dumpManager.registerCriticalDumpable(this) @@ -87,11 +85,7 @@ constructor( lastAppearanceRegions = originalAppearanceRegions lastLetterboxes = letterboxes return getLetterboxAppearanceInternal( - letterboxes, - originalAppearance, - originalAppearanceRegions, - statusBarBounds - ) + letterboxes, originalAppearance, originalAppearanceRegions, statusBarBounds) .also { lastLetterboxAppearance = it } } @@ -144,9 +138,7 @@ constructor( // full bounds of its window. // Here we want the bounds to be only for the inner bounds of the letterboxed app. AppearanceRegion( - appearanceRegion.appearance, - matchingLetterbox.letterboxInnerBounds - ) + appearanceRegion.appearance, matchingLetterbox.letterboxInnerBounds) } } @@ -156,8 +148,7 @@ constructor( ): LetterboxAppearance { return LetterboxAppearance( originalAppearance or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS, - originalAppearanceRegions - ) + originalAppearanceRegions) } @Appearance @@ -224,9 +215,7 @@ constructor( lastAppearanceRegion: $lastAppearanceRegions, lastLetterboxes: $lastLetterboxes, lastLetterboxAppearance: $lastLetterboxAppearance - """ - .trimIndent() - ) + """.trimIndent()) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 507759c037ba..68163b28dd09 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -325,7 +325,7 @@ class MobileIconInteractorImpl( } .stateIn(scope, SharingStarted.WhileSubscribed(), true) - private val shownLevel: StateFlow<Int> = + private val cellularShownLevel: StateFlow<Int> = combine( level, isInService, @@ -337,15 +337,19 @@ class MobileIconInteractorImpl( } .stateIn(scope, SharingStarted.WhileSubscribed(), 0) + // Satellite level is unaffected by the isInService or inflateSignalStrength properties + // See b/346904529 for details + private val satelliteShownLevel: StateFlow<Int> = level + private val cellularIcon: Flow<SignalIconModel.Cellular> = combine( - shownLevel, + cellularShownLevel, numberOfLevels, showExclamationMark, carrierNetworkChangeActive, - ) { shownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange -> + ) { cellularShownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange -> SignalIconModel.Cellular( - shownLevel, + cellularShownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange, @@ -353,7 +357,7 @@ class MobileIconInteractorImpl( } private val satelliteIcon: Flow<SignalIconModel.Satellite> = - shownLevel.map { + satelliteShownLevel.map { SignalIconModel.Satellite( level = it, icon = @@ -365,7 +369,7 @@ class MobileIconInteractorImpl( override val signalLevelIcon: StateFlow<SignalIconModel> = run { val initial = SignalIconModel.Cellular( - shownLevel.value, + cellularShownLevel.value, numberOfLevels.value, showExclamationMark.value, carrierNetworkChangeActive.value, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt index 8aabdf2a40f2..43ab3376e485 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt @@ -44,7 +44,6 @@ constructor(dumpManager: DumpManager, private val tag = "AvalancheController" private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG) - var enableAtRuntime = true // HUN showing right now, in the floating state where full shade is hidden, on launcher or AOD @VisibleForTesting var headsUpEntryShowing: HeadsUpEntry? = null @@ -87,17 +86,13 @@ constructor(dumpManager: DumpManager, dumpManager.registerNormalDumpable(tag, /* module */ this) } - fun isEnabled() : Boolean { - return NotificationThrottleHun.isEnabled && enableAtRuntime - } - fun getShowingHunKey(): String { return getKey(headsUpEntryShowing) } /** Run or delay Runnable for given HeadsUpEntry */ fun update(entry: HeadsUpEntry?, runnable: Runnable, label: String) { - if (!isEnabled()) { + if (!NotificationThrottleHun.isEnabled) { runnable.run() return } @@ -153,7 +148,7 @@ constructor(dumpManager: DumpManager, * all Runnables associated with that entry. */ fun delete(entry: HeadsUpEntry?, runnable: Runnable, label: String) { - if (!isEnabled()) { + if (!NotificationThrottleHun.isEnabled) { runnable.run() return } @@ -194,7 +189,7 @@ constructor(dumpManager: DumpManager, * BaseHeadsUpManager.HeadsUpEntry.calculateFinishTime to shorten display duration. */ fun getDurationMs(entry: HeadsUpEntry, autoDismissMs: Int): Int { - if (!isEnabled()) { + if (!NotificationThrottleHun.isEnabled) { // Use default duration, like we did before AvalancheController existed return autoDismissMs } @@ -243,7 +238,7 @@ constructor(dumpManager: DumpManager, /** Return true if entry is waiting to show. */ fun isWaiting(key: String): Boolean { - if (!isEnabled()) { + if (!NotificationThrottleHun.isEnabled) { return false } for (entry in nextMap.keys) { @@ -256,7 +251,7 @@ constructor(dumpManager: DumpManager, /** Return list of keys for huns waiting */ fun getWaitingKeys(): MutableList<String> { - if (!isEnabled()) { + if (!NotificationThrottleHun.isEnabled) { return mutableListOf() } val keyList = mutableListOf<String>() @@ -267,7 +262,7 @@ constructor(dumpManager: DumpManager, } fun getWaitingEntry(key: String): HeadsUpEntry? { - if (!isEnabled()) { + if (!NotificationThrottleHun.isEnabled) { return null } for (headsUpEntry in nextMap.keys) { @@ -279,7 +274,7 @@ constructor(dumpManager: DumpManager, } fun getWaitingEntryList(): List<HeadsUpEntry> { - if (!isEnabled()) { + if (!NotificationThrottleHun.isEnabled) { return mutableListOf() } return nextMap.keys.toList() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt index 2ce2bc83aa25..81e41d677be9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt @@ -45,6 +45,10 @@ import com.android.systemui.qs.tiles.impl.location.domain.LocationTileMapper import com.android.systemui.qs.tiles.impl.location.domain.interactor.LocationTileDataInteractor import com.android.systemui.qs.tiles.impl.location.domain.interactor.LocationTileUserActionInteractor import com.android.systemui.qs.tiles.impl.location.domain.model.LocationTileModel +import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileDataInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.android.systemui.qs.tiles.impl.modes.ui.ModesTileMapper import com.android.systemui.qs.tiles.impl.sensorprivacy.SensorPrivacyToggleTileDataInteractor import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.SensorPrivacyToggleTileUserActionInteractor import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.model.SensorPrivacyToggleTileModel @@ -85,35 +89,35 @@ interface PolicyModule { @IntoMap @StringKey(FLASHLIGHT_TILE_SPEC) fun provideAirplaneModeAvailabilityInteractor( - impl: FlashlightTileDataInteractor + impl: FlashlightTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(LOCATION_TILE_SPEC) fun provideLocationAvailabilityInteractor( - impl: LocationTileDataInteractor + impl: LocationTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(ALARM_TILE_SPEC) fun provideAlarmAvailabilityInteractor( - impl: AlarmTileDataInteractor + impl: AlarmTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(UIMODENIGHT_TILE_SPEC) fun provideUiModeNightAvailabilityInteractor( - impl: UiModeNightTileDataInteractor + impl: UiModeNightTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(WORK_MODE_TILE_SPEC) fun provideWorkModeAvailabilityInteractor( - impl: WorkModeTileDataInteractor + impl: WorkModeTileDataInteractor ): QSTileAvailabilityInteractor companion object { @@ -125,6 +129,7 @@ interface PolicyModule { const val CAMERA_TOGGLE_TILE_SPEC = "cameratoggle" const val MIC_TOGGLE_TILE_SPEC = "mictoggle" const val DND_TILE_SPEC = "dnd" + const val MODES_TILE_SPEC = "modes" /** Inject flashlight config */ @Provides @@ -327,7 +332,7 @@ interface PolicyModule { @IntoMap @StringKey(CAMERA_TOGGLE_TILE_SPEC) fun provideCameraToggleAvailabilityInteractor( - factory: SensorPrivacyToggleTileDataInteractor.Factory + factory: SensorPrivacyToggleTileDataInteractor.Factory ): QSTileAvailabilityInteractor { return factory.create(CAMERA) } @@ -369,12 +374,11 @@ interface PolicyModule { @IntoMap @StringKey(MIC_TOGGLE_TILE_SPEC) fun provideMicToggleModeAvailabilityInteractor( - factory: SensorPrivacyToggleTileDataInteractor.Factory + factory: SensorPrivacyToggleTileDataInteractor.Factory ): QSTileAvailabilityInteractor { return factory.create(MICROPHONE) } - /** Inject microphone toggle config */ @Provides @IntoMap @@ -389,6 +393,37 @@ interface PolicyModule { ), instanceId = uiEventLogger.getNewInstanceId(), ) + + @Provides + @IntoMap + @StringKey(MODES_TILE_SPEC) + fun provideModesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(MODES_TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.qs_dnd_icon_off, + labelRes = R.string.quick_settings_modes_label, + ), + instanceId = uiEventLogger.getNewInstanceId(), + ) + + /** Inject ModesTile into tileViewModelMap in QSModule */ + @Provides + @IntoMap + @StringKey(MODES_TILE_SPEC) + fun provideModesTileViewModel( + factory: QSTileViewModelFactory.Static<ModesTileModel>, + mapper: ModesTileMapper, + stateInteractor: ModesTileDataInteractor, + userActionInteractor: ModesTileUserActionInteractor + ): QSTileViewModel = + factory.create( + TileSpec.create(MODES_TILE_SPEC), + userActionInteractor, + stateInteractor, + mapper, + ) } /** Inject FlashlightTile into tileMap in QSModule */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt index d120a1bd3840..72d093c65a91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt @@ -22,10 +22,10 @@ interface SplitShadeStateController { /** Returns true if the device should use the split notification shade. */ @Deprecated( - message = "This is deprecated, please use ShadeInteractor#isSplitShade instead", + message = "This is deprecated, please use ShadeInteractor#shadeMode instead", replaceWith = ReplaceWith( - "shadeInteractor.isSplitShade", + "shadeInteractor.shadeMode", "com.android.systemui.shade.domain.interactor.ShadeInteractor", ), ) diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunablePadding.java b/packages/SystemUI/src/com/android/systemui/tuner/TunablePadding.java deleted file mode 100644 index d54c07c87b40..000000000000 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunablePadding.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2017 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.tuner; - -import android.util.DisplayMetrics; -import android.view.View; -import android.view.WindowManager; - -import com.android.systemui.Dependency; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.tuner.TunerService.Tunable; - -import javax.inject.Inject; - -/** - * Version of Space that can be resized by a tunable setting. - */ -public class TunablePadding implements Tunable { - - public static final int FLAG_START = 1; - public static final int FLAG_END = 2; - public static final int FLAG_TOP = 4; - public static final int FLAG_BOTTOM = 8; - - private final int mFlags; - private final View mView; - private final int mDefaultSize; - private final float mDensity; - private final TunerService mTunerService; - - private TunablePadding(String key, int def, int flags, View view, TunerService tunerService) { - mDefaultSize = def; - mFlags = flags; - mView = view; - DisplayMetrics metrics = new DisplayMetrics(); - view.getContext().getSystemService(WindowManager.class) - .getDefaultDisplay().getMetrics(metrics); - mDensity = metrics.density; - mTunerService = tunerService; - mTunerService.addTunable(this, key); - } - - @Override - public void onTuningChanged(String key, String newValue) { - int dimen = mDefaultSize; - if (newValue != null) { - try { - dimen = (int) (Integer.parseInt(newValue) * mDensity); - } catch (NumberFormatException ex) {} - } - int left = mView.isLayoutRtl() ? FLAG_END : FLAG_START; - int right = mView.isLayoutRtl() ? FLAG_START : FLAG_END; - mView.setPadding(getPadding(dimen, left), getPadding(dimen, FLAG_TOP), - getPadding(dimen, right), getPadding(dimen, FLAG_BOTTOM)); - } - - private int getPadding(int dimen, int flag) { - return ((mFlags & flag) != 0) ? dimen : 0; - } - - public void destroy() { - mTunerService.removeTunable(this); - } - - /** - * Exists for easy injecting in tests. - */ - @SysUISingleton - public static class TunablePaddingService { - - private final TunerService mTunerService; - - /** - */ - @Inject - public TunablePaddingService(TunerService tunerService) { - mTunerService = tunerService; - } - - public TunablePadding add(View view, String key, int defaultSize, int flags) { - if (view == null) { - throw new IllegalArgumentException(); - } - return new TunablePadding(key, defaultSize, flags, view, mTunerService); - } - } - - public static TunablePadding addTunablePadding(View view, String key, int defaultSize, - int flags) { - return Dependency.get(TunablePaddingService.class).add(view, key, defaultSize, flags); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt b/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt index ec7aabb58b49..f2132248e4f7 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt +++ b/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt @@ -20,6 +20,7 @@ import android.graphics.Rect import android.util.IndentingPrintWriter import android.view.View import android.view.ViewGroup +import dagger.Lazy import java.io.PrintWriter /** [Sequence] that yields all of the direct children of this [ViewGroup] */ @@ -56,3 +57,8 @@ val View.boundsOnScreen: Rect getBoundsOnScreen(bounds) return bounds } + +/** Extension method to convert [dagger.Lazy] to [kotlin.Lazy] for object of any class [T]. */ +fun <T> Lazy<T>.toKotlinLazy(): kotlin.Lazy<T> { + return lazy { this.get() } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt index ee00e8b04ef1..e6e2a0767012 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt @@ -35,3 +35,17 @@ fun ReduceBrightColorsController.isEnabled(): Flow<Boolean> { } .onStart { emit(isReduceBrightColorsActivated) } } + +fun ReduceBrightColorsController.isAvailable(): Flow<Boolean> { + return conflatedCallbackFlow { + val callback = + object : ReduceBrightColorsController.Listener { + override fun onFeatureEnabledChanged(enabled: Boolean) { + trySend(enabled) + } + } + addCallback(callback) + awaitClose { removeCallback(callback) } + } + .onStart { emit(isReduceBrightColorsFeatureAvailable) } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt index fe540445793e..b5934ec680d3 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt @@ -19,6 +19,7 @@ import android.content.ContentResolver import android.database.ContentObserver import android.net.Uri import android.provider.Settings.SettingNotFoundException +import androidx.annotation.AnyThread import androidx.annotation.WorkerThread import com.android.app.tracing.TraceUtils.trace import kotlinx.coroutines.CoroutineDispatcher @@ -57,7 +58,7 @@ interface SettingsProxy { * @param name to look up in the table * @return the corresponding content URI, or null if not present */ - fun getUriFor(name: String): Uri + @AnyThread fun getUriFor(name: String): Uri /** * Registers listener for a given content observer <b>while blocking the current thread</b>. @@ -89,12 +90,31 @@ interface SettingsProxy { * * API corresponding to [registerContentObserver] for Java usage. */ + @AnyThread fun registerContentObserverAsync(name: String, settingsObserver: ContentObserver) = CoroutineScope(backgroundDispatcher).launch { registerContentObserverSync(getUriFor(name), settingsObserver) } /** + * Convenience wrapper around [ContentResolver.registerContentObserver].' + * + * API corresponding to [registerContentObserver] for Java usage. After registration is + * complete, the callback block is called on the <b>background thread</b> to allow for update of + * value. + */ + @AnyThread + fun registerContentObserverAsync( + name: String, + settingsObserver: ContentObserver, + @WorkerThread registered: Runnable + ) = + CoroutineScope(backgroundDispatcher).launch { + registerContentObserverSync(getUriFor(name), settingsObserver) + registered.run() + } + + /** * Registers listener for a given content observer <b>while blocking the current thread</b>. * * This should not be called from the main thread, use [registerContentObserver] or @@ -120,6 +140,7 @@ interface SettingsProxy { * * API corresponding to [registerContentObserver] for Java usage. */ + @AnyThread fun registerContentObserverAsync(uri: Uri, settingsObserver: ContentObserver) = CoroutineScope(backgroundDispatcher).launch { registerContentObserverSync(uri, settingsObserver) @@ -128,8 +149,27 @@ interface SettingsProxy { /** * Convenience wrapper around [ContentResolver.registerContentObserver].' * + * API corresponding to [registerContentObserver] for Java usage. After registration is + * complete, the callback block is called on the <b>background thread</b> to allow for update of + * value. + */ + @AnyThread + fun registerContentObserverAsync( + uri: Uri, + settingsObserver: ContentObserver, + @WorkerThread registered: Runnable + ) = + CoroutineScope(backgroundDispatcher).launch { + registerContentObserverSync(uri, settingsObserver) + registered.run() + } + + /** + * Convenience wrapper around [ContentResolver.registerContentObserver].' + * * Implicitly calls [getUriFor] on the passed in name. */ + @WorkerThread fun registerContentObserverSync( name: String, notifyForDescendants: Boolean, @@ -158,6 +198,7 @@ interface SettingsProxy { * * API corresponding to [registerContentObserver] for Java usage. */ + @AnyThread fun registerContentObserverAsync( name: String, notifyForDescendants: Boolean, @@ -168,6 +209,25 @@ interface SettingsProxy { } /** + * Convenience wrapper around [ContentResolver.registerContentObserver].' + * + * API corresponding to [registerContentObserver] for Java usage. After registration is + * complete, the callback block is called on the <b>background thread</b> to allow for update of + * value. + */ + @AnyThread + fun registerContentObserverAsync( + name: String, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver, + @WorkerThread registered: Runnable + ) = + CoroutineScope(backgroundDispatcher).launch { + registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver) + registered.run() + } + + /** * Registers listener for a given content observer <b>while blocking the current thread</b>. * * This should not be called from the main thread, use [registerContentObserver] or @@ -207,6 +267,7 @@ interface SettingsProxy { * * API corresponding to [registerContentObserver] for Java usage. */ + @AnyThread fun registerContentObserverAsync( uri: Uri, notifyForDescendants: Boolean, @@ -217,6 +278,25 @@ interface SettingsProxy { } /** + * Convenience wrapper around [ContentResolver.registerContentObserver].' + * + * API corresponding to [registerContentObserver] for Java usage. After registration is + * complete, the callback block is called on the <b>background thread</b> to allow for update of + * value. + */ + @AnyThread + fun registerContentObserverAsync( + uri: Uri, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver, + @WorkerThread registered: Runnable + ) = + CoroutineScope(backgroundDispatcher).launch { + registerContentObserverSync(uri, notifyForDescendants, settingsObserver) + registered.run() + } + + /** * Unregisters the given content observer <b>while blocking the current thread</b>. * * This should not be called from the main thread, use [unregisterContentObserver] or @@ -246,6 +326,7 @@ interface SettingsProxy { * API corresponding to [unregisterContentObserver] for Java usage to ensure that * [ContentObserver] registration happens on a worker thread. */ + @AnyThread fun unregisterContentObserverAsync(settingsObserver: ContentObserver) = CoroutineScope(backgroundDispatcher).launch { unregisterContentObserver(settingsObserver) } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt index 3bf5b6511eb3..025354b51133 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt @@ -21,6 +21,7 @@ import android.database.ContentObserver import android.net.Uri import android.os.UserHandle import android.provider.Settings.SettingNotFoundException +import androidx.annotation.WorkerThread import com.android.app.tracing.TraceUtils.trace import com.android.systemui.settings.UserTracker import com.android.systemui.util.settings.SettingsProxy.Companion.parseFloat @@ -199,6 +200,24 @@ interface UserSettingsProxy : SettingsProxy { } /** + * Convenience wrapper around [ContentResolver.registerContentObserver].' + * + * API corresponding to [registerContentObserverForUser] for Java usage. After registration is + * complete, the callback block is called on the <b>background thread</b> to allow for update of + * value. + */ + fun registerContentObserverForUserAsync( + uri: Uri, + settingsObserver: ContentObserver, + userHandle: Int, + @WorkerThread registered: Runnable + ) = + CoroutineScope(backgroundDispatcher).launch { + registerContentObserverForUserSync(uri, settingsObserver, userHandle) + registered.run() + } + + /** * Convenience wrapper around [ContentResolver.registerContentObserver] * * Implicitly calls [getUriFor] on the passed in name. diff --git a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java index bd698ab8ad5c..bb230e6b0305 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java +++ b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java @@ -30,21 +30,31 @@ import android.content.IntentFilter; import android.media.AudioManager; import android.provider.Settings; import android.util.Log; +import android.util.Pair; import android.view.KeyEvent; import android.view.WindowManager; +import androidx.annotation.VisibleForTesting; + import com.android.internal.annotations.GuardedBy; import com.android.internal.messages.nano.SystemMessageProto; +import com.android.systemui.Flags; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.NotificationChannels; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.google.common.collect.ImmutableList; + import dagger.assisted.Assisted; import dagger.assisted.AssistedFactory; import dagger.assisted.AssistedInject; +import java.util.Optional; + /** * A class that implements the three Computed Sound Dose-related warnings defined in * {@link AudioManager}: @@ -75,6 +85,9 @@ public class CsdWarningDialog extends SystemUIDialog private static final int KEY_CONFIRM_ALLOWED_AFTER_MS = 1000; // milliseconds // time after which action is taken when the user hasn't ack'd or dismissed the dialog public static final int NO_ACTION_TIMEOUT_MS = 5000; + // Notification dismiss intent + private static final String DISMISS_CSD_NOTIFICATION = + "com.android.systemui.volume.DISMISS_CSD_NOTIFICATION"; private final Context mContext; private final AudioManager mAudioManager; @@ -95,21 +108,32 @@ public class CsdWarningDialog extends SystemUIDialog private long mShowTime; + @VisibleForTesting public int mCachedMediaStreamVolume; + private Optional<ImmutableList<Pair<String, Intent>>> mActionIntents; + private final BroadcastDispatcher mBroadcastDispatcher; + /** * To inject dependencies and allow for easier testing */ @AssistedFactory public interface Factory { - /** - * Create a dialog object - */ - CsdWarningDialog create(int csdWarning, Runnable onCleanup); + /** Create a dialog object */ + CsdWarningDialog create( + int csdWarning, + Runnable onCleanup, + Optional<ImmutableList<Pair<String, Intent>>> actionIntents); } @AssistedInject - public CsdWarningDialog(@Assisted @AudioManager.CsdWarning int csdWarning, Context context, - AudioManager audioManager, NotificationManager notificationManager, - @Background DelayableExecutor delayableExecutor, @Assisted Runnable onCleanup) { + public CsdWarningDialog( + @Assisted @AudioManager.CsdWarning int csdWarning, + Context context, + AudioManager audioManager, + NotificationManager notificationManager, + @Background DelayableExecutor delayableExecutor, + @Assisted Runnable onCleanup, + @Assisted Optional<ImmutableList<Pair<String, Intent>>> actionIntents, + BroadcastDispatcher broadcastDispatcher) { super(context); mCsdWarning = csdWarning; mContext = context; @@ -118,7 +142,8 @@ public class CsdWarningDialog extends SystemUIDialog mOnCleanup = onCleanup; mDelayableExecutor = delayableExecutor; - + mActionIntents = actionIntents; + mBroadcastDispatcher = broadcastDispatcher; getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR); setShowForAllUsers(true); setMessage(mContext.getString(getStringForWarning(csdWarning))); @@ -133,14 +158,17 @@ public class CsdWarningDialog extends SystemUIDialog Context.RECEIVER_EXPORTED_UNAUDITED); if (csdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) { - mNoUserActionRunnable = () -> { - if (mCsdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) { - // unlike on the 5x dose repeat, level is only reduced to RS1 when the warning - // is not acknowledged quickly enough - mAudioManager.lowerVolumeToRs1(); - sendNotification(/*for5XCsd=*/false); - } - }; + mNoUserActionRunnable = + () -> { + if (mCsdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) { + // unlike on the 5x dose repeat, level is only reduced to RS1 when the + // warning is not acknowledged quickly enough + mCachedMediaStreamVolume = + mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + mAudioManager.lowerVolumeToRs1(); + sendNotification(/* for5XCsd= */ false); + } + }; } else { mNoUserActionRunnable = null; } @@ -242,6 +270,38 @@ public class CsdWarningDialog extends SystemUIDialog } }; + @VisibleForTesting + public final BroadcastReceiver mReceiverUndo = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Flags.sounddoseCustomization() + && VolumeDialog.ACTION_VOLUME_UNDO.equals(intent.getAction())) { + if (D.BUG) Log.d(TAG, "Received ACTION_VOLUME_UNDO"); + mAudioManager.setStreamVolume( + AudioManager.STREAM_MUSIC, + mCachedMediaStreamVolume, + AudioManager.FLAG_SHOW_UI); + mNotificationManager.cancel( + SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO); + destroy(); + } + } + }; + + @VisibleForTesting + public final BroadcastReceiver mReceiverDismissNotification = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Flags.sounddoseCustomization() + && DISMISS_CSD_NOTIFICATION.equals(intent.getAction())) { + if (D.BUG) Log.d(TAG, "Received DISMISS_CSD_NOTIFICATION"); + destroy(); + } + } + }; + private @StringRes int getStringForWarning(@AudioManager.CsdWarning int csdWarning) { switch (csdWarning) { case AudioManager.CSD_WARNING_DOSE_REACHED_1X: @@ -259,7 +319,7 @@ public class CsdWarningDialog extends SystemUIDialog Log.w(TAG, "Notification dose repeat 5x is not shown for " + mCsdWarning); return; } - + mCachedMediaStreamVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); mAudioManager.lowerVolumeToRs1(); sendNotification(/*for5XCsd=*/true); } @@ -288,7 +348,62 @@ public class CsdWarningDialog extends SystemUIDialog .setAutoCancel(true) .setCategory(Notification.CATEGORY_SYSTEM); + if (Flags.sounddoseCustomization() + && mActionIntents.isPresent() + && !mActionIntents.get().isEmpty()) { + ImmutableList<Pair<String, Intent>> actionIntentsList = mActionIntents.get(); + for (Pair<String, Intent> intentPair : actionIntentsList) { + if (intentPair != null && intentPair.first != null && intentPair.second != null) { + PendingIntent pendingActionIntent = + PendingIntent.getBroadcast(mContext, 0, intentPair.second, + FLAG_IMMUTABLE); + builder.addAction(0, intentPair.first, pendingActionIntent); + // Register receiver to undo volume only when + // notification conaining the undo action would be sent. + if (intentPair.first == mContext.getString(R.string.volume_undo_action)) { + final IntentFilter filterUndo = new IntentFilter( + VolumeDialog.ACTION_VOLUME_UNDO); + mBroadcastDispatcher.registerReceiver(mReceiverUndo, + filterUndo, + /* executor = default */ null, + /* user = default */ null, + Context.RECEIVER_NOT_EXPORTED, + /* permission = default */ null); + + // Register receiver to learn if notification has been dismissed. + // This is required to unregister receivers to prevent leak. + Intent dismissIntent = new Intent(DISMISS_CSD_NOTIFICATION) + .setPackage(mContext.getPackageName()); + PendingIntent pendingDismissIntent = PendingIntent.getBroadcast(mContext, + 0, dismissIntent, FLAG_IMMUTABLE); + mBroadcastDispatcher.registerReceiver(mReceiverDismissNotification, + new IntentFilter(DISMISS_CSD_NOTIFICATION), + /* executor = default */ null, + /* user = default */ null, + Context.RECEIVER_NOT_EXPORTED, + /* permission = default */ null); + builder.setDeleteIntent(pendingDismissIntent); + } + } + } + } + mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO, builder.build()); } + + /** + * Unregister the Undo action receiver when the notification is dismissed. + * Unregister cannot happen when CsdWarning dialog is dismissed + * as the notification lifecycle would be longer. + */ + @VisibleForTesting + public void destroy() { + if (Flags.sounddoseCustomization() + && mActionIntents.isPresent() + && !mActionIntents.get().isEmpty()) { + mBroadcastDispatcher.unregisterReceiver(mReceiverUndo); + mBroadcastDispatcher.unregisterReceiver(mReceiverDismissNotification); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 795d427b43bf..6b02e1ada491 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -50,6 +50,7 @@ import android.app.KeyguardManager; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.Configuration; @@ -77,6 +78,7 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.text.InputFilter; import android.util.Log; +import android.util.Pair; import android.util.Slog; import android.util.SparseBooleanArray; import android.view.ContextThemeWrapper; @@ -140,11 +142,14 @@ import com.android.systemui.volume.domain.interactor.VolumePanelNavigationIntera import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag; import com.android.systemui.volume.ui.navigation.VolumeNavigator; +import com.google.common.collect.ImmutableList; + import dagger.Lazy; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; /** @@ -315,6 +320,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private final com.android.systemui.util.time.SystemClock mSystemClock; private final VolumePanelFlag mVolumePanelFlag; private final VolumeDialogInteractor mInteractor; + // Optional actions for soundDose + private Optional<ImmutableList<Pair<String, Intent>>> mCsdWarningNotificationActions = + Optional.of(ImmutableList.of()); public VolumeDialogImpl( Context context, @@ -2198,6 +2206,11 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, rescheduleTimeoutH(); } + public void setCsdWarningNotificationActionIntents( + ImmutableList<Pair<String, Intent>> actionIntent) { + mCsdWarningNotificationActions = Optional.of(actionIntent); + } + @VisibleForTesting void showCsdWarningH(int csdWarning, int durationMs) { synchronized (mSafetyWarningLock) { @@ -2212,7 +2225,8 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, recheckH(null); }; - mCsdDialog = mCsdWarningDialogFactory.create(csdWarning, cleanUp); + mCsdDialog = mCsdWarningDialogFactory.create( + csdWarning, cleanUp, mCsdWarningNotificationActions); mCsdDialog.show(); } recheckH(null); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index c30bedde557d..12140b58936b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -288,7 +288,7 @@ public class MenuViewLayerTest extends SysuiTestCase { mockActivityQuery(true); mMenuViewLayer.dispatchAccessibilityAction(R.id.action_edit); ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); - verify(mSpyContext).startActivity(intentCaptor.capture()); + verify(mSpyContext).startActivityAsUser(intentCaptor.capture(), eq(UserHandle.CURRENT)); assertThat(intentCaptor.getValue().getAction()).isEqualTo( mMenuViewLayer.getIntentForEditScreen().getAction()); } @@ -299,6 +299,7 @@ public class MenuViewLayerTest extends SysuiTestCase { mockActivityQuery(false); mMenuViewLayer.dispatchAccessibilityAction(R.id.action_edit); verify(mSpyContext, never()).startActivity(any()); + verify(mSpyContext, never()).startActivityAsUser(any(), any()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java index 8f7dc7cf109b..5ea5c2189560 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java @@ -16,8 +16,9 @@ package com.android.systemui.accessibility.hearingaid; +import static android.bluetooth.BluetoothHapClient.PRESET_INDEX_UNAVAILABLE; + import static com.android.systemui.accessibility.hearingaid.HearingDevicesDialogDelegate.LIVE_CAPTION_INTENT; -import static com.android.systemui.statusbar.phone.SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK; import static com.google.common.truth.Truth.assertThat; @@ -26,9 +27,13 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHapPresetInfo; +import android.bluetooth.BluetoothProfile; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -42,6 +47,7 @@ import android.provider.Settings; import android.testing.TestableLooper; import android.view.View; import android.widget.LinearLayout; +import android.widget.Spinner; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -86,14 +92,15 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { public MockitoRule mockito = MockitoJUnit.rule(); private static final String DEVICE_ADDRESS = "AA:BB:CC:DD:EE:FF"; + private static final String DEVICE_NAME = "test_name"; private static final String TEST_PKG = "pkg"; private static final String TEST_CLS = "cls"; private static final ComponentName TEST_COMPONENT = new ComponentName(TEST_PKG, TEST_CLS); private static final String TEST_LABEL = "label"; + private static final int TEST_PRESET_INDEX = 1; + private static final String TEST_PRESET_NAME = "test_preset"; @Mock - private SystemUIDialog.Factory mSystemUIDialogFactory; - @Mock private SystemUIDialogManager mSystemUIDialogManager; @Mock private SysUiState mSysUiState; @@ -118,6 +125,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { @Mock private CachedBluetoothDevice mCachedDevice; @Mock + private BluetoothDevice mDevice; + @Mock private DeviceItem mHearingDeviceItem; @Mock private PackageManager mPackageManager; @@ -125,7 +134,10 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { private ActivityInfo mActivityInfo; @Mock private Drawable mDrawable; + @Mock + private HearingDevicesPresetsController mPresetsController; private SystemUIDialog mDialog; + private SystemUIDialog.Factory mDialogFactory; private HearingDevicesDialogDelegate mDialogDelegate; private TestableLooper mTestableLooper; private final List<CachedBluetoothDevice> mDevices = new ArrayList<>(); @@ -141,23 +153,23 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mDevices); when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState); + when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + when(mDevice.isConnected()).thenReturn(true); + when(mCachedDevice.getDevice()).thenReturn(mDevice); when(mCachedDevice.getAddress()).thenReturn(DEVICE_ADDRESS); + when(mCachedDevice.getName()).thenReturn(DEVICE_NAME); + when(mCachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); + when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice.isConnectedHapClientDevice()).thenReturn(true); when(mHearingDeviceItem.getCachedBluetoothDevice()).thenReturn(mCachedDevice); - mContext.setMockPackageManager(mPackageManager); - - setUpPairNewDeviceDialog(); - when(mSystemUIDialogFactory.create(any(SystemUIDialog.Delegate.class))) - .thenReturn(mDialog); - } - - @Test - public void createDialog_dialogShown() { - assertThat(mDialogDelegate.createDialog()).isEqualTo(mDialog); + mContext.setMockPackageManager(mPackageManager); + mDevices.add(mCachedDevice); } @Test public void clickPairNewDeviceButton_intentActionMatch() { + setUpPairNewDeviceDialog(); mDialog.show(); getPairNewDeviceButton(mDialog).performClick(); @@ -185,6 +197,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { @Test public void onDeviceItemOnClicked_connectedDevice_disconnect() { + setUpDeviceListDialog(); when(mHearingDeviceItem.getType()).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE); mDialogDelegate.onDeviceItemOnClicked(mHearingDeviceItem, new View(mContext)); @@ -231,50 +244,100 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { assertThat(relatedToolsView.getChildCount()).isEqualTo(2); } + @Test + public void showDialog_noPreset_presetGone() { + when(mPresetsController.getAllPresetInfo()).thenReturn(new ArrayList<>()); + when(mPresetsController.getActivePresetIndex()).thenReturn(PRESET_INDEX_UNAVAILABLE); + + setUpDeviceListDialog(); + mDialog.show(); + + Spinner spinner = (Spinner) getPresetSpinner(mDialog); + assertThat(spinner.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void showDialog_presetExist_presetSelected() { + BluetoothHapPresetInfo info = getTestPresetInfo(); + when(mPresetsController.getAllPresetInfo()).thenReturn(List.of(info)); + when(mPresetsController.getActivePresetIndex()).thenReturn(TEST_PRESET_INDEX); + + setUpDeviceListDialog(); + mDialog.show(); + + Spinner spinner = (Spinner) getPresetSpinner(mDialog); + assertThat(spinner.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(spinner.getSelectedItemPosition()).isEqualTo(0); + } + + @Test + public void onActiveDeviceChanged_presetExist_presetSelected() { + setUpDeviceListDialog(); + mDialog.show(); + BluetoothHapPresetInfo info = getTestPresetInfo(); + when(mPresetsController.getAllPresetInfo()).thenReturn(List.of(info)); + when(mPresetsController.getActivePresetIndex()).thenReturn(TEST_PRESET_INDEX); + + mDialogDelegate.onActiveDeviceChanged(mCachedDevice, BluetoothProfile.LE_AUDIO); + mTestableLooper.processAllMessages(); + + Spinner spinner = (Spinner) getPresetSpinner(mDialog); + assertThat(spinner.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(spinner.getSelectedItemPosition()).isEqualTo(0); + } + + + private void setUpPairNewDeviceDialog() { + mDialogFactory = new SystemUIDialog.Factory( + mContext, + mSystemUIDialogManager, + mSysUiState, + getFakeBroadcastDispatcher(), + mDialogTransitionAnimator + ); mDialogDelegate = new HearingDevicesDialogDelegate( mContext, true, - mSystemUIDialogFactory, + mDialogFactory, mActivityStarter, mDialogTransitionAnimator, mLocalBluetoothManager, new Handler(mTestableLooper.getLooper()), mAudioManager ); - mDialog = new SystemUIDialog( + + mDialog = mDialogDelegate.createDialog(); + } + + private void setUpDeviceListDialog() { + mDialogFactory = new SystemUIDialog.Factory( mContext, - 0, - DEFAULT_DISMISS_ON_DEVICE_LOCK, mSystemUIDialogManager, mSysUiState, getFakeBroadcastDispatcher(), - mDialogTransitionAnimator, - mDialogDelegate + mDialogTransitionAnimator ); - } - - private void setUpDeviceListDialog() { mDialogDelegate = new HearingDevicesDialogDelegate( mContext, false, - mSystemUIDialogFactory, + mDialogFactory, mActivityStarter, mDialogTransitionAnimator, mLocalBluetoothManager, new Handler(mTestableLooper.getLooper()), mAudioManager ); - mDialog = new SystemUIDialog( - mContext, - 0, - DEFAULT_DISMISS_ON_DEVICE_LOCK, - mSystemUIDialogManager, - mSysUiState, - getFakeBroadcastDispatcher(), - mDialogTransitionAnimator, - mDialogDelegate - ); + + mDialog = mDialogDelegate.createDialog(); + mDialogDelegate.setHearingDevicesPresetsController(mPresetsController); + } + + private BluetoothHapPresetInfo getTestPresetInfo() { + BluetoothHapPresetInfo info = mock(BluetoothHapPresetInfo.class); + when(info.getName()).thenReturn(TEST_PRESET_NAME); + when(info.getIndex()).thenReturn(TEST_PRESET_INDEX); + return info; } private View getPairNewDeviceButton(SystemUIDialog dialog) { @@ -285,6 +348,10 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { return dialog.requireViewById(R.id.related_tools_container); } + private View getPresetSpinner(SystemUIDialog dialog) { + return dialog.requireViewById(R.id.preset_spinner); + } + @After public void reset() { if (mDialogDelegate != null) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt index 7094848e3127..e60848bb6bc3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt @@ -120,11 +120,26 @@ class CommunalBackupHelperTest : SysuiTestCase() { private fun setUpDatabase(): List<FakeWidgetMetadata> { return listOf( - FakeWidgetMetadata(11, "com.android.fakePackage1/fakeWidget1", 3), - FakeWidgetMetadata(12, "com.android.fakePackage2/fakeWidget2", 2), - FakeWidgetMetadata(13, "com.android.fakePackage3/fakeWidget3", 1), + FakeWidgetMetadata( + widgetId = 11, + componentName = "com.android.fakePackage1/fakeWidget1", + rank = 3, + userSerialNumber = 0, + ), + FakeWidgetMetadata( + widgetId = 12, + componentName = "com.android.fakePackage2/fakeWidget2", + rank = 2, + userSerialNumber = 0, + ), + FakeWidgetMetadata( + widgetId = 13, + componentName = "com.android.fakePackage3/fakeWidget3", + rank = 1, + userSerialNumber = 10, + ), ) - .onEach { dao.addWidget(it.widgetId, it.componentName, it.rank) } + .onEach { dao.addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber) } } private fun getBackupDataInputStream(): BackupDataInputStream { diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt index cde7a0ed1bd4..983a43561590 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt @@ -66,11 +66,28 @@ class CommunalBackupUtilsTest : SysuiTestCase() { // Set up database val expectedWidgets = listOf( - FakeWidgetMetadata(11, "com.android.fakePackage1/fakeWidget1", 3), - FakeWidgetMetadata(12, "com.android.fakePackage2/fakeWidget2", 2), - FakeWidgetMetadata(13, "com.android.fakePackage3/fakeWidget3", 1), + FakeWidgetMetadata( + widgetId = 11, + componentName = "com.android.fakePackage1/fakeWidget1", + rank = 3, + userSerialNumber = 0, + ), + FakeWidgetMetadata( + widgetId = 12, + componentName = "com.android.fakePackage2/fakeWidget2", + rank = 2, + userSerialNumber = 0, + ), + FakeWidgetMetadata( + widgetId = 13, + componentName = "com.android.fakePackage3/fakeWidget3", + rank = 1, + userSerialNumber = 10, + ), ) - expectedWidgets.forEach { dao.addWidget(it.widgetId, it.componentName, it.rank) } + expectedWidgets.forEach { + dao.addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber) + } // Get communal hub state val state = underTest.getCommunalHubState() @@ -128,7 +145,12 @@ class CommunalBackupUtilsTest : SysuiTestCase() { assertThat(underTest.fileExists()).isFalse() } - data class FakeWidgetMetadata(val widgetId: Int, val componentName: String, val rank: Int) + data class FakeWidgetMetadata( + val widgetId: Int, + val componentName: String, + val rank: Int, + val userSerialNumber: Int, + ) companion object { /** @@ -140,7 +162,8 @@ class CommunalBackupUtilsTest : SysuiTestCase() { { actual, expected -> actual?.widgetId == expected?.widgetId && actual?.componentName == expected?.componentName && - actual?.rank == expected?.rank + actual?.rank == expected?.rank && + actual?.userSerialNumber == expected?.userSerialNumber }, "represents", ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt new file mode 100644 index 000000000000..eb0ab782ae3f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.db + +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.systemui.SysuiTestCase +import com.android.systemui.lifecycle.InstantTaskExecutorRule +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalDatabaseMigrationsTest : SysuiTestCase() { + + @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() + + @get:Rule + val migrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + CommunalDatabase::class.java.canonicalName, + ) + + @Test + fun migrate1To2() { + // Create a communal database in version 1 + val databaseV1 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 1) + + // Populate some fake data + val fakeWidgetsV1 = + listOf( + FakeCommunalWidgetItemV1(1, "test_widget_1", 11), + FakeCommunalWidgetItemV1(2, "test_widget_2", 12), + FakeCommunalWidgetItemV1(3, "test_widget_3", 13), + ) + databaseV1.insertWidgetsV1(fakeWidgetsV1) + + // Verify fake widgets populated + databaseV1.verifyWidgetsV1(fakeWidgetsV1) + + // Run migration and get database V2, the migration test helper verifies that the schema is + // updated correctly + val databaseV2 = + migrationTestHelper.runMigrationsAndValidate( + name = DATABASE_NAME, + version = 2, + validateDroppedTables = false, + CommunalDatabase.MIGRATION_1_2, + ) + + // Verify data is migrated correctly + databaseV2.verifyWidgetsV2(fakeWidgetsV1.map { it.getV2() }) + } + + private fun SupportSQLiteDatabase.insertWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) { + widgets.forEach { widget -> + execSQL( + "INSERT INTO communal_widget_table(widget_id, component_name, item_id) " + + "VALUES(${widget.widgetId}, '${widget.componentName}', ${widget.itemId})" + ) + } + } + + private fun SupportSQLiteDatabase.verifyWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) { + val cursor = query("SELECT * FROM communal_widget_table") + assertThat(cursor.moveToFirst()).isTrue() + + widgets.forEach { widget -> + assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId) + assertThat(cursor.getString(cursor.getColumnIndex("component_name"))) + .isEqualTo(widget.componentName) + assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId) + + cursor.moveToNext() + } + + // Verify there is no more columns + assertThat(cursor.isAfterLast).isTrue() + } + + private fun SupportSQLiteDatabase.verifyWidgetsV2(widgets: List<FakeCommunalWidgetItemV2>) { + val cursor = query("SELECT * FROM communal_widget_table") + assertThat(cursor.moveToFirst()).isTrue() + + widgets.forEach { widget -> + assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId) + assertThat(cursor.getString(cursor.getColumnIndex("component_name"))) + .isEqualTo(widget.componentName) + assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId) + assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number"))) + .isEqualTo(widget.userSerialNumber) + + cursor.moveToNext() + } + + // Verify there is no more columns + assertThat(cursor.isAfterLast).isTrue() + } + + /** + * Returns the expected data after migration from V1 to V2, which is simply that the new user + * serial number field is now set to [CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED]. + */ + private fun FakeCommunalWidgetItemV1.getV2(): FakeCommunalWidgetItemV2 { + return FakeCommunalWidgetItemV2( + widgetId, + componentName, + itemId, + CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED, + ) + } + + private data class FakeCommunalWidgetItemV1( + val widgetId: Int, + val componentName: String, + val itemId: Int, + ) + + private data class FakeCommunalWidgetItemV2( + val widgetId: Int, + val componentName: String, + val itemId: Int, + val userSerialNumber: Int, + ) + + companion object { + private const val DATABASE_NAME = "communal_db" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt index f77c7a672ae3..d6705085eafd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt @@ -67,11 +67,12 @@ class CommunalWidgetDaoTest : SysuiTestCase() { @Test fun addWidget_readValueInDb() = testScope.runTest { - val (widgetId, provider, priority) = widgetInfo1 + val (widgetId, provider, priority, userSerialNumber) = widgetInfo1 communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, priority = priority, + userSerialNumber = userSerialNumber, ) val entry = communalWidgetDao.getWidgetByIdNow(id = 1) assertThat(entry).isEqualTo(communalWidgetItemEntry1) @@ -80,11 +81,12 @@ class CommunalWidgetDaoTest : SysuiTestCase() { @Test fun deleteWidget_notInDb_returnsFalse() = testScope.runTest { - val (widgetId, provider, priority) = widgetInfo1 + val (widgetId, provider, priority, userSerialNumber) = widgetInfo1 communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, priority = priority, + userSerialNumber = userSerialNumber, ) assertThat(communalWidgetDao.deleteWidgetById(widgetId = 123)).isFalse() } @@ -95,11 +97,12 @@ class CommunalWidgetDaoTest : SysuiTestCase() { val widgetsToAdd = listOf(widgetInfo1, widgetInfo2) val widgets = collectLastValue(communalWidgetDao.getWidgets()) widgetsToAdd.forEach { - val (widgetId, provider, priority) = it + val (widgetId, provider, priority, userSerialNumber) = it communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, priority = priority, + userSerialNumber = userSerialNumber ) } assertThat(widgets()) @@ -118,11 +121,12 @@ class CommunalWidgetDaoTest : SysuiTestCase() { val widgets = collectLastValue(communalWidgetDao.getWidgets()) widgetsToAdd.forEach { - val (widgetId, provider, priority) = it + val (widgetId, provider, priority, userSerialNumber) = it communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, priority = priority, + userSerialNumber = userSerialNumber, ) } assertThat(widgets()) @@ -144,11 +148,12 @@ class CommunalWidgetDaoTest : SysuiTestCase() { val widgets = collectLastValue(communalWidgetDao.getWidgets()) widgetsToAdd.forEach { - val (widgetId, provider, priority) = it + val (widgetId, provider, priority, userSerialNumber) = it communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, priority = priority, + userSerialNumber = userSerialNumber, ) } assertThat(widgets()) @@ -180,11 +185,12 @@ class CommunalWidgetDaoTest : SysuiTestCase() { val widgets = collectLastValue(communalWidgetDao.getWidgets()) existingWidgets.forEach { - val (widgetId, provider, priority) = it + val (widgetId, provider, priority, userSerialNumber) = it communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, priority = priority, + userSerialNumber = userSerialNumber, ) } assertThat(widgets()) @@ -212,6 +218,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { widgetId = widgetInfo3.widgetId, provider = widgetInfo3.provider, priority = 2, + userSerialNumber = widgetInfo3.userSerialNumber, ) assertThat(widgets()) .containsExactly( @@ -246,6 +253,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { widgetId = fakeWidget.widgetId, componentName = fakeWidget.componentName, itemId = rank.uid, + userSerialNumber = fakeWidget.userSerialNumber, ) expected[rank] = widget } @@ -258,13 +266,15 @@ class CommunalWidgetDaoTest : SysuiTestCase() { widgetId = metadata.widgetId, provider = metadata.provider, priority = priority ?: metadata.priority, + userSerialNumber = metadata.userSerialNumber, ) } data class FakeWidgetMetadata( val widgetId: Int, val provider: ComponentName, - val priority: Int + val priority: Int, + val userSerialNumber: Int, ) companion object { @@ -272,19 +282,22 @@ class CommunalWidgetDaoTest : SysuiTestCase() { FakeWidgetMetadata( widgetId = 1, provider = ComponentName("pk_name", "cls_name_1"), - priority = 1 + priority = 1, + userSerialNumber = 0, ) val widgetInfo2 = FakeWidgetMetadata( widgetId = 2, provider = ComponentName("pk_name", "cls_name_2"), - priority = 2 + priority = 2, + userSerialNumber = 0, ) val widgetInfo3 = FakeWidgetMetadata( widgetId = 3, provider = ComponentName("pk_name", "cls_name_3"), - priority = 3 + priority = 3, + userSerialNumber = 10, ) val communalItemRankEntry1 = CommunalItemRank(uid = 1L, rank = widgetInfo1.priority) val communalItemRankEntry2 = CommunalItemRank(uid = 2L, rank = widgetInfo2.priority) @@ -295,6 +308,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { widgetId = widgetInfo1.widgetId, componentName = widgetInfo1.provider.flattenToString(), itemId = communalItemRankEntry1.uid, + userSerialNumber = widgetInfo1.userSerialNumber, ) val communalWidgetItemEntry2 = CommunalWidgetItem( @@ -302,6 +316,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { widgetId = widgetInfo2.widgetId, componentName = widgetInfo2.provider.flattenToString(), itemId = communalItemRankEntry2.uid, + userSerialNumber = widgetInfo2.userSerialNumber, ) val communalWidgetItemEntry3 = CommunalWidgetItem( @@ -309,6 +324,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { widgetId = widgetInfo3.widgetId, componentName = widgetInfo3.provider.flattenToString(), itemId = communalItemRankEntry3.uid, + userSerialNumber = widgetInfo3.userSerialNumber, ) val fakeState = CommunalHubState().apply { @@ -318,11 +334,13 @@ class CommunalWidgetDaoTest : SysuiTestCase() { widgetId = 1 componentName = "pk_name/fake_widget_1" rank = 1 + userSerialNumber = 0 }, CommunalHubState.CommunalWidgetItem().apply { widgetId = 2 componentName = "pk_name/fake_widget_2" rank = 2 + userSerialNumber = 10 }, ) .toTypedArray() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt new file mode 100644 index 000000000000..14837f219862 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt @@ -0,0 +1,90 @@ +/* + * 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.keyboard.shortcut.data.repository + +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.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts +import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesRepository +import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() { + + private val fakeSystemSource = FakeKeyboardShortcutGroupsSource() + private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource() + + private val kosmos = + testKosmos().also { + it.testDispatcher = UnconfinedTestDispatcher() + it.shortcutHelperSystemShortcutsSource = fakeSystemSource + it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource + it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() + } + + private val repo = kosmos.shortcutHelperCategoriesRepository + private val helper = kosmos.shortcutHelperTestHelper + private val testScope = kosmos.testScope + + @Before + fun setUp() { + fakeSystemSource.setGroups(TestShortcuts.systemGroups) + fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups) + } + + @Test + fun categories_multipleSubscribers_replaysExistingValueToNewSubscribers() = + testScope.runTest { + fakeSystemSource.setGroups(TestShortcuts.systemGroups) + fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups) + helper.showFromActivity() + val firstCategories by collectLastValue(repo.categories) + + // Intentionally change shortcuts now. This simulates "current app" shortcuts changing + // when our helper is shown. + // We still want to return the shortcuts that were returned before our helper was + // showing. + fakeSystemSource.setGroups(emptyList()) + + val secondCategories by collectLastValue(repo.categories) + // Make sure the second subscriber receives the same value as the first subscriber, even + // though fetching shortcuts again would have returned a new result. + assertThat(secondCategories).isEqualTo(firstCategories) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index 765cd014864c..4fba7e355df8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -220,7 +220,7 @@ object TestShortcuts { val imeGroups = listOf(standardGroup1, standardGroup2, standardGroup3) val imeCategory = ShortcutCategory( - type = ShortcutCategoryType.IME, + type = ShortcutCategoryType.InputMethodEditor, subCategories = listOf( subCategoryForInputLanguageSwitchShortcuts, @@ -233,14 +233,14 @@ object TestShortcuts { val systemGroups = listOf(standardGroup3, standardGroup2, standardGroup1) val systemCategory = ShortcutCategory( - type = ShortcutCategoryType.SYSTEM, + type = ShortcutCategoryType.System, subCategories = listOf(standardSubCategory3, standardSubCategory2, standardSubCategory1) ) val multitaskingGroups = listOf(standardGroup2, standardGroup1) val multitaskingCategory = ShortcutCategory( - type = ShortcutCategoryType.MULTI_TASKING, + type = ShortcutCategoryType.MultiTasking, subCategories = listOf(standardSubCategory2, standardSubCategory1) ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt index 4c1e8696cdc1..57c8b444b922 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt @@ -23,11 +23,12 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.IME -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MULTI_TASKING -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.SYSTEM +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesInteractor +import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper @@ -48,14 +49,14 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { private val systemShortcutsSource = FakeKeyboardShortcutGroupsSource() private val multitaskingShortcutsSource = FakeKeyboardShortcutGroupsSource() - private val defaultAppsShortcutsSource = FakeKeyboardShortcutGroupsSource() @OptIn(ExperimentalCoroutinesApi::class) private val kosmos = testKosmos().also { it.testDispatcher = UnconfinedTestDispatcher() it.shortcutHelperSystemShortcutsSource = systemShortcutsSource it.shortcutHelperMultiTaskingShortcutsSource = multitaskingShortcutsSource - it.shortcutHelperAppCategoriesShortcutsSource = defaultAppsShortcutsSource + it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() } private val testScope = kosmos.testScope @@ -117,7 +118,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { TestShortcuts.systemCategory, TestShortcuts.multitaskingCategory, ShortcutCategory( - type = IME, + type = InputMethodEditor, subCategories = TestShortcuts.imeSubCategoriesWithGroupedDuplicatedShortcutLabels ), @@ -137,7 +138,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { assertThat(categories) .containsExactly( ShortcutCategory( - type = SYSTEM, + type = System, subCategories = TestShortcuts.subCategoriesWithGroupedDuplicatedShortcutLabels ), @@ -160,7 +161,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { .containsExactly( TestShortcuts.systemCategory, ShortcutCategory( - type = MULTI_TASKING, + type = MultiTasking, subCategories = TestShortcuts.subCategoriesWithGroupedDuplicatedShortcutLabels ), @@ -182,7 +183,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { TestShortcuts.systemCategory, TestShortcuts.multitaskingCategory, ShortcutCategory( - type = IME, + type = InputMethodEditor, subCategories = TestShortcuts.imeSubCategoriesWithUnsupportedModifiersRemoved ), @@ -201,7 +202,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { assertThat(categories) .containsExactly( ShortcutCategory( - type = SYSTEM, + type = System, subCategories = TestShortcuts.subCategoriesWithUnsupportedModifiersRemoved ), TestShortcuts.multitaskingCategory, @@ -222,7 +223,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { .containsExactly( TestShortcuts.systemCategory, ShortcutCategory( - type = MULTI_TASKING, + type = MultiTasking, subCategories = TestShortcuts.subCategoriesWithUnsupportedModifiersRemoved ), TestShortcuts.imeCategory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt index 0757ea156bbf..f8e2f47f939a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt @@ -20,8 +20,15 @@ import android.content.Intent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity import com.android.systemui.keyboard.shortcut.shortcutHelperActivityStarter +import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity import com.android.systemui.kosmos.Kosmos @@ -32,6 +39,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -40,10 +48,18 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShortcutHelperActivityStarterTest : SysuiTestCase() { + private val fakeSystemSource = FakeKeyboardShortcutGroupsSource() + private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource() + private val kosmos = Kosmos().also { it.testCase = this it.testDispatcher = UnconfinedTestDispatcher() + it.shortcutHelperSystemShortcutsSource = fakeSystemSource + it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource + it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() } private val testScope = kosmos.testScope @@ -51,6 +67,12 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { private val fakeStartActivity = kosmos.fakeShortcutHelperStartActivity private val starter = kosmos.shortcutHelperActivityStarter + @Before + fun setUp() { + fakeSystemSource.setGroups(TestShortcuts.systemGroups) + fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups) + } + @Test fun start_doesNotStartByDefault() = testScope.runTest { @@ -70,13 +92,30 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { } @Test + fun start_onToggle_noShortcuts_doesNotStartActivity() = + testScope.runTest { + fakeSystemSource.setGroups(emptyList()) + fakeMultiTaskingSource.setGroups(emptyList()) + + starter.start() + + testHelper.toggle(deviceId = 456) + + assertThat(fakeStartActivity.startIntents).isEmpty() + } + + @Test fun start_onToggle_multipleTimesStartsActivityOnlyWhenNotStarted() = testScope.runTest { starter.start() + // Starts testHelper.toggle(deviceId = 456) + // Stops testHelper.toggle(deviceId = 456) + // Starts again testHelper.toggle(deviceId = 456) + // Stops testHelper.toggle(deviceId = 456) verifyShortcutHelperActivityStarted(numTimes = 2) @@ -93,6 +132,18 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { } @Test + fun start_onRequestShowShortcuts_noShortcuts_doesNotStartActivity() = + testScope.runTest { + fakeSystemSource.setGroups(emptyList()) + fakeMultiTaskingSource.setGroups(emptyList()) + starter.start() + + testHelper.showFromActivity() + + assertThat(fakeStartActivity.startIntents).isEmpty() + } + + @Test fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyOnce() = testScope.runTest { starter.start() @@ -109,13 +160,21 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { testScope.runTest { starter.start() + // No-op. Already hidden. testHelper.hideFromActivity() + // No-op. Already hidden. testHelper.hideForSystem() + // Show 1st time. testHelper.toggle(deviceId = 987) + // No-op. Already shown. testHelper.showFromActivity() + // Hidden. testHelper.hideFromActivity() + // No-op. Already hidden. testHelper.hideForSystem() + // Show 2nd time. testHelper.toggle(deviceId = 456) + // No-op. Already shown. testHelper.showFromActivity() verifyShortcutHelperActivityStarted(numTimes = 2) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt index 80d487cab50d..07feaa1a5047 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt @@ -21,6 +21,13 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts +import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource +import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState @@ -34,6 +41,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -42,10 +50,18 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShortcutHelperViewModelTest : SysuiTestCase() { + private val fakeSystemSource = FakeKeyboardShortcutGroupsSource() + private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource() + private val kosmos = Kosmos().also { it.testCase = this it.testDispatcher = UnconfinedTestDispatcher() + it.shortcutHelperSystemShortcutsSource = fakeSystemSource + it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource + it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() } private val testScope = kosmos.testScope @@ -53,6 +69,12 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { private val sysUiState = kosmos.sysUiState private val viewModel = kosmos.shortcutHelperViewModel + @Before + fun setUp() { + fakeSystemSource.setGroups(TestShortcuts.systemGroups) + fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups) + } + @Test fun shouldShow_falseByDefault() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt index f0ad5103e9a6..59f16d70fab5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt @@ -36,7 +36,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.clocks.ClockConfig import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever @@ -84,7 +83,7 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { fun testAppliesDefaultBlueprint() { testScope.runTest { val blueprintId by collectLastValue(underTest.blueprintId) - kosmos.shadeRepository.setShadeMode(ShadeMode.Single) + kosmos.shadeRepository.setShadeLayoutWide(false) configurationRepository.onConfigurationChange() runCurrent() @@ -98,7 +97,7 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { fun testAppliesSplitShadeBlueprint() { testScope.runTest { val blueprintId by collectLastValue(underTest.blueprintId) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) configurationRepository.onConfigurationChange() runCurrent() @@ -112,7 +111,7 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { fun testDoesNotApplySplitShadeBlueprint() { testScope.runTest { val blueprintId by collectLastValue(underTest.blueprintId) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + kosmos.shadeRepository.setShadeLayoutWide(true) clockRepository.setCurrentClock(clockController) configurationRepository.onConfigurationChange() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt index 22181f8fa568..7e249e8c179d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt @@ -24,10 +24,12 @@ import android.content.Intent import android.content.mockedContext import android.os.PowerManager import android.os.UserHandle +import android.platform.test.annotations.EnableFlags import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.lockPatternUtils +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -78,6 +80,7 @@ class KeyguardWakeDirectlyToGoneInteractorTest : SysuiTestCase() { private val transitionRepository = kosmos.fakeKeyguardTransitionRepository @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testCanWakeDirectlyToGone_keyguardServiceEnabledThenDisabled() = testScope.runTest { val canWake by collectValues(underTest.canWakeDirectlyToGone) @@ -114,6 +117,7 @@ class KeyguardWakeDirectlyToGoneInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testCanWakeDirectlyToGone_lockscreenDisabledThenEnabled() = testScope.runTest { val canWake by collectValues(underTest.canWakeDirectlyToGone) @@ -178,6 +182,7 @@ class KeyguardWakeDirectlyToGoneInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testCanWakeDirectlyToGone_wakeAndUnlock() = testScope.runTest { val canWake by collectValues(underTest.canWakeDirectlyToGone) @@ -201,6 +206,7 @@ class KeyguardWakeDirectlyToGoneInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testCanWakeDirectlyToGone_andSetsAlarm_ifPowerButtonDoesNotLockImmediately() = testScope.runTest { val canWake by collectValues(underTest.canWakeDirectlyToGone) @@ -224,6 +230,7 @@ class KeyguardWakeDirectlyToGoneInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testSetsCanIgnoreAuth_andSetsAlarm_whenTimingOut() = testScope.runTest { val canWake by collectValues(underTest.canWakeDirectlyToGone) @@ -267,6 +274,7 @@ class KeyguardWakeDirectlyToGoneInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testCancelsFirstAlarm_onWake_withSecondAlarmSet() = testScope.runTest { val canWake by collectValues(underTest.canWakeDirectlyToGone) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt index 4a39a9b2e801..6e381caf5124 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt @@ -39,7 +39,6 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.policy.fakeConfigurationController import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy @@ -48,6 +47,7 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before @@ -57,6 +57,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @SmallTest class ClockSectionTest : SysuiTestCase() { @@ -127,7 +128,7 @@ class ClockSectionTest : SysuiTestCase() { fun testApplyDefaultConstraints_LargeClock_SplitShade() = kosmos.testScope.runTest { with(kosmos) { - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) keyguardClockInteractor.setClockSize(ClockSize.LARGE) advanceUntilIdle() } @@ -143,11 +144,11 @@ class ClockSectionTest : SysuiTestCase() { fun testApplyDefaultConstraints_LargeClock_NonSplitShade() = kosmos.testScope.runTest { with(kosmos) { - val collectedShadeMode by collectLastValue(shadeRepository.shadeMode) + val isShadeLayoutWide by collectLastValue(shadeRepository.isShadeLayoutWide) val isLargeClockVisible by collectLastValue(keyguardClockViewModel.isLargeClockVisible) - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) keyguardClockInteractor.setClockSize(ClockSize.LARGE) fakeKeyguardRepository.setClockShouldBeCentered(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) @@ -168,11 +169,11 @@ class ClockSectionTest : SysuiTestCase() { kosmos.testScope.runTest { with(kosmos) { DIMENSION_BY_IDENTIFIER = listOf() // Remove Smartspace from mock - val collectedShadeMode by collectLastValue(shadeRepository.shadeMode) + val isShadeLayoutWide by collectLastValue(shadeRepository.isShadeLayoutWide) val isLargeClockVisible by collectLastValue(keyguardClockViewModel.isLargeClockVisible) - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) keyguardClockInteractor.setClockSize(ClockSize.LARGE) fakeKeyguardRepository.setClockShouldBeCentered(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) @@ -193,11 +194,11 @@ class ClockSectionTest : SysuiTestCase() { kosmos.testScope.runTest { with(kosmos) { DIMENSION_BY_IDENTIFIER = listOf() // Remove Smartspace from mock - val collectedShadeMode by collectLastValue(shadeRepository.shadeMode) + val isShadeLayoutWide by collectLastValue(shadeRepository.isShadeLayoutWide) val isLargeClockVisible by collectLastValue(keyguardClockViewModel.isLargeClockVisible) - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) keyguardClockInteractor.setClockSize(ClockSize.LARGE) fakeKeyguardRepository.setClockShouldBeCentered(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) @@ -217,11 +218,11 @@ class ClockSectionTest : SysuiTestCase() { fun testApplyDefaultConstraints_SmallClock_SplitShade() = kosmos.testScope.runTest { with(kosmos) { - val collectedShadeMode by collectLastValue(shadeRepository.shadeMode) + val isShadeLayoutWide by collectLastValue(shadeRepository.isShadeLayoutWide) val isLargeClockVisible by collectLastValue(keyguardClockViewModel.isLargeClockVisible) - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) keyguardClockInteractor.setClockSize(ClockSize.SMALL) fakeKeyguardRepository.setClockShouldBeCentered(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) @@ -241,11 +242,11 @@ class ClockSectionTest : SysuiTestCase() { fun testApplyDefaultConstraints_SmallClock_NonSplitShade() = kosmos.testScope.runTest { with(kosmos) { - val collectedShadeMode by collectLastValue(shadeRepository.shadeMode) + val isShadeLayoutWide by collectLastValue(shadeRepository.isShadeLayoutWide) val isLargeClockVisible by collectLastValue(keyguardClockViewModel.isLargeClockVisible) - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) keyguardClockInteractor.setClockSize(ClockSize.SMALL) fakeKeyguardRepository.setClockShouldBeCentered(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) @@ -334,7 +335,7 @@ class ClockSectionTest : SysuiTestCase() { } companion object { - private val SMART_SPACE_DATE_WEATHER_HEIGHT = 10 - private val ENHANCED_SMART_SPACE_HEIGHT = 11 + private const val SMART_SPACE_DATE_WEATHER_HEIGHT = 10 + private const val ENHANCED_SMART_SPACE_HEIGHT = 11 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt index 40663ceb2ad2..17e1b53a3ba9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt @@ -38,7 +38,6 @@ import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever @@ -88,7 +87,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() val currentClockLayout by collectLastValue(underTest.currentClockLayout) with(kosmos) { - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) keyguardRepository.setClockShouldBeCentered(true) keyguardClockRepository.setClockSize(ClockSize.LARGE) } @@ -103,7 +102,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() val currentClockLayout by collectLastValue(underTest.currentClockLayout) with(kosmos) { - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) keyguardRepository.setClockShouldBeCentered(false) keyguardClockRepository.setClockSize(ClockSize.LARGE) } @@ -118,7 +117,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() val currentClockLayout by collectLastValue(underTest.currentClockLayout) with(kosmos) { - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) keyguardRepository.setClockShouldBeCentered(false) keyguardClockRepository.setClockSize(ClockSize.SMALL) } @@ -133,7 +132,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() val currentClockLayout by collectLastValue(underTest.currentClockLayout) with(kosmos) { - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) keyguardClockRepository.setClockSize(ClockSize.SMALL) } @@ -146,7 +145,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() val currentClockLayout by collectLastValue(underTest.currentClockLayout) with(kosmos) { - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) keyguardClockRepository.setClockSize(ClockSize.LARGE) } @@ -234,7 +233,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() fun testSmallClockTop_splitShade_composeLockscreenOn() = testScope.runTest { with(kosmos) { - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) fakeSystemBarUtilsProxy.fakeKeyguardStatusBarHeight = KEYGUARD_STATUS_BAR_HEIGHT } @@ -249,7 +248,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() fun testSmallClockTop_splitShade_composeLockscreenOff() = testScope.runTest { with(kosmos) { - shadeRepository.setShadeMode(ShadeMode.Split) + shadeRepository.setShadeLayoutWide(true) fakeSystemBarUtilsProxy.fakeKeyguardStatusBarHeight = KEYGUARD_STATUS_BAR_HEIGHT } @@ -262,7 +261,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() fun testSmallClockTop_nonSplitShade_composeLockscreenOn() = testScope.runTest { with(kosmos) { - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) fakeSystemBarUtilsProxy.fakeKeyguardStatusBarHeight = KEYGUARD_STATUS_BAR_HEIGHT } @@ -275,7 +274,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() fun testSmallClockTop_nonSplitShade_composeLockscreenOff() = testScope.runTest { with(kosmos) { - shadeRepository.setShadeMode(ShadeMode.Single) + shadeRepository.setShadeLayoutWide(false) fakeSystemBarUtilsProxy.fakeKeyguardStatusBarHeight = KEYGUARD_STATUS_BAR_HEIGHT } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt index c0d411b12496..5db898115f2d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt @@ -16,19 +16,25 @@ package com.android.systemui.mediaprojection.data.repository +import android.hardware.display.displayManager import android.media.projection.MediaProjectionInfo import android.os.Binder +import android.os.Handler import android.os.UserHandle import android.view.ContentRecordingSession +import android.view.Display 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.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createDisplaySession +import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeTasksRepository import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos @@ -36,7 +42,9 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest @@ -47,6 +55,7 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager + private val displayManager = kosmos.displayManager private val repo = kosmos.realMediaProjectionRepository @@ -139,6 +148,37 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { } @Test + fun mediaProjectionState_entireScreen_validVirtualDisplayId_hasHostDeviceName() = + testScope.runTest { + val state by collectLastValue(repo.mediaProjectionState) + + val session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123) + session.virtualDisplayId = 45 + val displayInfo = mock<Display>().apply { whenever(this.name).thenReturn("Test Name") } + whenever(displayManager.getDisplay(45)).thenReturn(displayInfo) + + fakeMediaProjectionManager.dispatchOnSessionSet(session = session) + + assertThat((state as MediaProjectionState.Projecting.EntireScreen).hostDeviceName) + .isEqualTo("Test Name") + } + + @Test + fun mediaProjectionState_entireScreen_invalidVirtualDisplayId_nullHostDeviceName() = + testScope.runTest { + val state by collectLastValue(repo.mediaProjectionState) + + val session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123) + session.virtualDisplayId = 45 + whenever(displayManager.getDisplay(45)).thenReturn(null) + + fakeMediaProjectionManager.dispatchOnSessionSet(session = session) + + assertThat((state as MediaProjectionState.Projecting.EntireScreen).hostDeviceName) + .isNull() + } + + @Test fun mediaProjectionState_sessionSet_taskWithToken_matchingRunningTask_emitsSingleTask() = testScope.runTest { val token = createToken() @@ -179,6 +219,89 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { } @Test + fun mediaProjectionState_singleTask_validVirtualDisplayId_hasHostDeviceName() = + testScope.runTest { + val state by collectLastValue(repo.mediaProjectionState) + + val token = createToken() + val task = createTask(taskId = 1, token = token) + fakeActivityTaskManager.addRunningTasks(task) + + val session = ContentRecordingSession.createTaskSession(token.asBinder()) + session.virtualDisplayId = 45 + val displayInfo = mock<Display>().apply { whenever(this.name).thenReturn("Test Name") } + whenever(displayManager.getDisplay(45)).thenReturn(displayInfo) + + fakeMediaProjectionManager.dispatchOnSessionSet(session = session) + + assertThat((state as MediaProjectionState.Projecting.SingleTask).hostDeviceName) + .isEqualTo("Test Name") + } + + @Test + fun mediaProjectionState_singleTask_invalidVirtualDisplayId_nullHostDeviceName() = + testScope.runTest { + val state by collectLastValue(repo.mediaProjectionState) + + val token = createToken() + val task = createTask(taskId = 1, token = token) + fakeActivityTaskManager.addRunningTasks(task) + + val session = ContentRecordingSession.createTaskSession(token.asBinder()) + session.virtualDisplayId = 45 + whenever(displayManager.getDisplay(45)).thenReturn(null) + + fakeMediaProjectionManager.dispatchOnSessionSet(session = session) + + assertThat((state as MediaProjectionState.Projecting.SingleTask).hostDeviceName) + .isNull() + } + + /** Regression test for b/352483752. */ + @Test + fun mediaProjectionState_sessionStartedThenImmediatelyStopped_emitsOnlyNotProjecting() = + testScope.runTest { + val fakeTasksRepo = FakeTasksRepository() + val repoWithTimingControl = + MediaProjectionManagerRepository( + // fakeTasksRepo lets us have control over when the background dispatcher + // finishes fetching the tasks info. + tasksRepository = fakeTasksRepo, + mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, + displayManager = displayManager, + handler = Handler.getMain(), + applicationScope = kosmos.applicationCoroutineScope, + backgroundDispatcher = kosmos.testDispatcher, + mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, + ) + + val state by collectLastValue(repoWithTimingControl.mediaProjectionState) + + val token = createToken() + val task = createTask(taskId = 1, token = token) + + // Dispatch a session using a task session so that MediaProjectionManagerRepository + // has to ask TasksRepository for the tasks info. + fakeMediaProjectionManager.dispatchOnSessionSet( + session = ContentRecordingSession.createTaskSession(token.asBinder()) + ) + // FakeTasksRepository is set up to not return the tasks info until the test manually + // calls [FakeTasksRepository#setRunningTaskResult]. At this point, + // MediaProjectionManagerRepository is waiting for the tasks info and hasn't emitted + // anything yet. + + // Before the tasks info comes back, dispatch a stop event. + fakeMediaProjectionManager.dispatchOnStop() + + // Then let the tasks info come back. + fakeTasksRepo.setRunningTaskResult(task) + + // Verify that MediaProjectionManagerRepository threw away the tasks info because + // a newer callback event (#onStop) occurred. + assertThat(state).isEqualTo(MediaProjectionState.NotProjecting) + } + + @Test fun stopProjecting_invokesManager() = testScope.runTest { repo.stopProjecting() diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt new file mode 100644 index 000000000000..ce2b9830951b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.mediaprojection.taskswitcher.data.repository + +import android.app.ActivityManager +import android.os.IBinder +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +/** + * Fake tasks repository that gives us fine-grained control over when the result of + * [findRunningTaskFromWindowContainerToken] gets emitted. + */ +class FakeTasksRepository : TasksRepository { + override suspend fun launchRecentTask(taskInfo: ActivityManager.RunningTaskInfo) {} + + private val findRunningTaskResult: CompletableDeferred<ActivityManager.RunningTaskInfo?> = + CompletableDeferred() + + override suspend fun findRunningTaskFromWindowContainerToken( + windowContainerToken: IBinder + ): ActivityManager.RunningTaskInfo? { + return findRunningTaskResult.await() + } + + fun setRunningTaskResult(task: ActivityManager.RunningTaskInfo?) { + findRunningTaskResult.complete(task) + } + + override val foregroundTask: Flow<ActivityManager.RunningTaskInfo> = emptyFlow() +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java index b0265c07363f..3621ab975daf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java @@ -103,7 +103,7 @@ public class NavigationBarTransitionsTest extends SysuiTestCase { public void setIsLightsOut_AutoDim() { mTransitions.setAutoDim(true); - assertTrue(mTransitions.isLightsOut(BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT)); + assertTrue(mTransitions.isLightsOut(BarTransitions.MODE_OPAQUE)); assertTrue(mTransitions.isLightsOut(BarTransitions.MODE_LIGHTS_OUT)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java index 16a022f720ae..8681123d0c2b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java @@ -276,8 +276,8 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mLauncherApps = mock(LauncherApps.class); - mManager = new PeopleSpaceWidgetManager(mContext, mAppWidgetManager, mIPeopleManager, - mPeopleManager, mLauncherApps, mNotifCollection, mPackageManager, + mManager = new PeopleSpaceWidgetManager(mContext, Optional.of(mAppWidgetManager), + mIPeopleManager, mPeopleManager, mLauncherApps, mNotifCollection, mPackageManager, Optional.of(mBubbles), mUserManager, mBackupManager, mINotificationManager, mNotificationManager, mFakeExecutor, mUserTracker); mManager.attach(mListenerService); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java index 798e9fb208b7..d6bde27dfb62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.Handler; +import android.platform.test.annotations.RequiresFlagsDisabled; import android.service.quicksettings.Tile; import android.testing.TestableLooper; @@ -32,6 +33,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.internal.logging.MetricsLogger; +import com.android.server.display.feature.flags.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.plugins.ActivityStarter; @@ -131,6 +133,7 @@ public class ReduceBrightColorsTileTest extends SysuiTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER) public void testActive_clicked_featureIsActivated() { when(mReduceBrightColorsController.isReduceBrightColorsActivated()).thenReturn(false); mTile.refreshState(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt index 0b81b5e6de35..8d3a29ac7278 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt @@ -69,7 +69,7 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { @Before fun setUp() { - whenever(controllerFactory.create(any(), any())).thenReturn(controller) + whenever(controllerFactory.create(any())).thenReturn(controller) whenever(notificationControllerFactory.create(eq(0))).thenReturn(notificationsController0) whenever(notificationControllerFactory.create(eq(1))).thenReturn(notificationsController1) } @@ -83,8 +83,8 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { val onSaved = { _: Uri? -> } screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback) - verify(controllerFactory).create(eq(internalDisplay), any()) - verify(controllerFactory, never()).create(eq(externalDisplay), any()) + verify(controllerFactory).create(eq(internalDisplay)) + verify(controllerFactory, never()).create(eq(externalDisplay)) val capturer = ArgumentCaptor<ScreenshotData>() @@ -118,8 +118,8 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { callback ) - verify(controllerFactory).create(eq(internalDisplay), any()) - verify(controllerFactory, never()).create(eq(externalDisplay), any()) + verify(controllerFactory).create(eq(internalDisplay)) + verify(controllerFactory, never()).create(eq(externalDisplay)) val capturer = ArgumentCaptor<ScreenshotData>() @@ -151,7 +151,7 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { @Test fun executeScreenshots_allowedTypes_allCaptured() = testScope.runTest { - whenever(controllerFactory.create(any(), any())).thenReturn(controller) + whenever(controllerFactory.create(any())).thenReturn(controller) setDisplays( display(TYPE_INTERNAL, id = 0), diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java index baf1357a1ae0..193d29c1d550 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java @@ -334,13 +334,17 @@ public final class AppClipsViewModelTest extends SysuiTestCase { } private void resetPackageManagerMockingForUsingFallbackBacklinks() { + ResolveInfo backlinksTaskResolveInfo = createBacklinksTaskResolveInfo(); reset(mPackageManager); when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE); when(mPackageManager.resolveActivity(any(Intent.class), anyInt())) - // First the logic queries whether a package has a launcher activity, this should + // Firstly, the logic queries whether a package has a launcher activity, this should // resolve otherwise the logic filters out the task. - .thenReturn(createBacklinksTaskResolveInfo()) - // Then logic queries with the backlinks intent, this should not resolve for the + .thenReturn(backlinksTaskResolveInfo) + // Secondly, the logic builds a fallback main launcher intent, this should also + // resolve for the fallback intent to build correctly. + .thenReturn(backlinksTaskResolveInfo) + // Lastly, logic queries with the backlinks intent, this should not resolve for the // logic to use the fallback intent. .thenReturn(null); } @@ -360,6 +364,8 @@ public final class AppClipsViewModelTest extends SysuiTestCase { assertThat(actualBacklinksIntent.getPackage()).isEqualTo(BACKLINKS_TASK_PACKAGE_NAME); assertThat(actualBacklinksIntent.getAction()).isEqualTo(ACTION_MAIN); assertThat(actualBacklinksIntent.getCategories()).containsExactly(CATEGORY_LAUNCHER); + assertThat(actualBacklinksIntent.getComponent()).isEqualTo( + new ComponentName(BACKLINKS_TASK_PACKAGE_NAME, BACKLINKS_TASK_APP_NAME)); } private static ResolveInfo createBacklinksTaskResolveInfo() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt index 97acc6e53cdf..7d4918a30d9c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt @@ -19,8 +19,8 @@ package com.android.systemui.shade.data.repository 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.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -28,7 +28,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class ShadeRepositoryImplTest : SysuiTestCase() { @@ -91,37 +90,37 @@ class ShadeRepositoryImplTest : SysuiTestCase() { @Test fun updateLegacyShadeTracking() = testScope.runTest { - assertThat(underTest.legacyShadeTracking.value).isEqualTo(false) + assertThat(underTest.legacyShadeTracking.value).isFalse() underTest.setLegacyShadeTracking(true) - assertThat(underTest.legacyShadeTracking.value).isEqualTo(true) + assertThat(underTest.legacyShadeTracking.value).isTrue() } @Test fun updateLegacyLockscreenShadeTracking() = testScope.runTest { - assertThat(underTest.legacyLockscreenShadeTracking.value).isEqualTo(false) + assertThat(underTest.legacyLockscreenShadeTracking.value).isFalse() underTest.setLegacyLockscreenShadeTracking(true) - assertThat(underTest.legacyLockscreenShadeTracking.value).isEqualTo(true) + assertThat(underTest.legacyLockscreenShadeTracking.value).isTrue() } @Test fun updateLegacyQsTracking() = testScope.runTest { - assertThat(underTest.legacyQsTracking.value).isEqualTo(false) + assertThat(underTest.legacyQsTracking.value).isFalse() underTest.setLegacyQsTracking(true) - assertThat(underTest.legacyQsTracking.value).isEqualTo(true) + assertThat(underTest.legacyQsTracking.value).isTrue() } @Test fun updateLegacyExpandedOrAwaitingInputTransfer() = testScope.runTest { - assertThat(underTest.legacyExpandedOrAwaitingInputTransfer.value).isEqualTo(false) + assertThat(underTest.legacyExpandedOrAwaitingInputTransfer.value).isFalse() underTest.setLegacyExpandedOrAwaitingInputTransfer(true) - assertThat(underTest.legacyExpandedOrAwaitingInputTransfer.value).isEqualTo(true) + assertThat(underTest.legacyExpandedOrAwaitingInputTransfer.value).isTrue() } @Test @@ -142,36 +141,46 @@ class ShadeRepositoryImplTest : SysuiTestCase() { @Test fun updateLegacyIsQsExpanded() = testScope.runTest { - assertThat(underTest.legacyIsQsExpanded.value).isEqualTo(false) + assertThat(underTest.legacyIsQsExpanded.value).isFalse() underTest.setLegacyIsQsExpanded(true) - assertThat(underTest.legacyIsQsExpanded.value).isEqualTo(true) + assertThat(underTest.legacyIsQsExpanded.value).isTrue() } @Test fun updateLegacyExpandImmediate() = testScope.runTest { - assertThat(underTest.legacyExpandImmediate.value).isEqualTo(false) + assertThat(underTest.legacyExpandImmediate.value).isFalse() underTest.setLegacyExpandImmediate(true) - assertThat(underTest.legacyExpandImmediate.value).isEqualTo(true) + assertThat(underTest.legacyExpandImmediate.value).isTrue() } @Test fun updateLegacyQsFullscreen() = testScope.runTest { - assertThat(underTest.legacyQsFullscreen.value).isEqualTo(false) + assertThat(underTest.legacyQsFullscreen.value).isFalse() underTest.setLegacyQsFullscreen(true) - assertThat(underTest.legacyQsFullscreen.value).isEqualTo(true) + assertThat(underTest.legacyQsFullscreen.value).isTrue() } @Test fun updateLegacyIsClosing() = testScope.runTest { - assertThat(underTest.legacyIsClosing.value).isEqualTo(false) + assertThat(underTest.legacyIsClosing.value).isFalse() underTest.setLegacyIsClosing(true) - assertThat(underTest.legacyIsClosing.value).isEqualTo(true) + assertThat(underTest.legacyIsClosing.value).isTrue() + } + + @Test + fun isShadeLayoutWide() = + testScope.runTest { + val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) + assertThat(isShadeLayoutWide).isFalse() + + underTest.setShadeLayoutWide(true) + assertThat(isShadeLayoutWide).isTrue() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt index c8397bdf47c7..5005d1609113 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt @@ -75,7 +75,9 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { @Test fun message_entireScreen_unknownDevice() { - createAndSetDelegate(ENTIRE_SCREEN, deviceName = null) + createAndSetDelegate( + MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE, hostDeviceName = null) + ) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) @@ -87,7 +89,12 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { @Test fun message_entireScreen_hasDevice() { - createAndSetDelegate(ENTIRE_SCREEN, deviceName = "My Favorite Device") + createAndSetDelegate( + MediaProjectionState.Projecting.EntireScreen( + HOST_PACKAGE, + hostDeviceName = "My Favorite Device" + ) + ) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) @@ -110,9 +117,9 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { createAndSetDelegate( MediaProjectionState.Projecting.SingleTask( HOST_PACKAGE, + hostDeviceName = null, createTask(taskId = 1, baseIntent = baseIntent) ), - deviceName = null, ) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) @@ -133,9 +140,9 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { createAndSetDelegate( MediaProjectionState.Projecting.SingleTask( HOST_PACKAGE, + hostDeviceName = "My Favorite Device", createTask(taskId = 1, baseIntent = baseIntent) ), - deviceName = "My Favorite Device", ) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) @@ -161,9 +168,9 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { createAndSetDelegate( MediaProjectionState.Projecting.SingleTask( HOST_PACKAGE, + hostDeviceName = null, createTask(taskId = 1, baseIntent = baseIntent) ), - deviceName = null, ) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) @@ -189,9 +196,9 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { createAndSetDelegate( MediaProjectionState.Projecting.SingleTask( HOST_PACKAGE, - createTask(taskId = 1, baseIntent = baseIntent) + hostDeviceName = "My Favorite Device", + createTask(taskId = 1, baseIntent = baseIntent), ), - deviceName = "My Favorite Device", ) underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null) @@ -240,10 +247,7 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue() } - private fun createAndSetDelegate( - state: MediaProjectionState.Projecting, - deviceName: String? = null, - ) { + private fun createAndSetDelegate(state: MediaProjectionState.Projecting) { underTest = EndCastScreenToOtherDeviceDialogDelegate( kosmos.endMediaProjectionDialogHelper, @@ -252,15 +256,19 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() { ProjectionChipModel.Projecting( ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE, state, - deviceName, ), ) } companion object { private const val HOST_PACKAGE = "fake.host.package" - private val ENTIRE_SCREEN = MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE) + private val ENTIRE_SCREEN = + MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE, hostDeviceName = null) private val SINGLE_TASK = - MediaProjectionState.Projecting.SingleTask(HOST_PACKAGE, createTask(taskId = 1)) + MediaProjectionState.Projecting.SingleTask( + HOST_PACKAGE, + hostDeviceName = null, + createTask(taskId = 1) + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt index fe29140143f9..c9a7c82d6b3f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue @@ -38,7 +37,6 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel -import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.policy.CastDevice @@ -47,9 +45,7 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before -import org.mockito.ArgumentMatchers import org.mockito.kotlin.any -import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -65,17 +61,6 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { private val mockScreenCastDialog = mock<SystemUIDialog>() private val mockGenericCastDialog = mock<SystemUIDialog>() - private val chipBackgroundView = mock<ChipBackgroundContainer>() - private val chipView = - mock<View>().apply { - whenever( - this.requireViewById<ChipBackgroundContainer>( - R.id.ongoing_activity_chip_background - ) - ) - .thenReturn(chipBackgroundView) - } - private val underTest = kosmos.castToOtherDeviceChipViewModel @Before @@ -116,6 +101,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( CAST_TO_OTHER_DEVICES_PACKAGE, + hostDeviceName = null, createTask(taskId = 1), ) @@ -223,7 +209,11 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chip) mediaProjectionRepo.mediaProjectionState.value = - MediaProjectionState.Projecting.SingleTask(NORMAL_PACKAGE, createTask(taskId = 1)) + MediaProjectionState.Projecting.SingleTask( + NORMAL_PACKAGE, + hostDeviceName = null, + createTask(taskId = 1), + ) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) } @@ -258,6 +248,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( CAST_TO_OTHER_DEVICES_PACKAGE, + hostDeviceName = null, createTask(taskId = 1), ) @@ -306,14 +297,8 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(chipView) - verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockScreenCastDialog), - eq(chipBackgroundView), - eq(null), - ArgumentMatchers.anyBoolean(), - ) + clickListener!!.onClick(mock<View>()) + verify(mockScreenCastDialog).show() } @Test @@ -324,20 +309,15 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( CAST_TO_OTHER_DEVICES_PACKAGE, + hostDeviceName = null, createTask(taskId = 1), ) val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(chipView) - verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockScreenCastDialog), - eq(chipBackgroundView), - eq(null), - ArgumentMatchers.anyBoolean(), - ) + clickListener!!.onClick(mock<View>()) + verify(mockScreenCastDialog).show() } @Test @@ -359,13 +339,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(chipView) - verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockGenericCastDialog), - eq(chipBackgroundView), - eq(null), - ArgumentMatchers.anyBoolean(), - ) + clickListener!!.onClick(mock<View>()) + verify(mockGenericCastDialog).show() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt index 7eb4ca06b80d..d0c5e7a102e0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt @@ -72,6 +72,7 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( CAST_TO_OTHER_DEVICES_PACKAGE, + hostDeviceName = null, createTask(taskId = 1) ) @@ -101,7 +102,11 @@ class MediaProjectionChipInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.projection) mediaProjectionRepo.mediaProjectionState.value = - MediaProjectionState.Projecting.SingleTask(NORMAL_PACKAGE, createTask(taskId = 1)) + MediaProjectionState.Projecting.SingleTask( + NORMAL_PACKAGE, + hostDeviceName = null, + createTask(taskId = 1), + ) assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java) assertThat((latest as ProjectionChipModel.Projecting).type) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt index ab935fe9b631..c62e40414121 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt @@ -72,6 +72,7 @@ class EndMediaProjectionDialogHelperTest : SysuiTestCase() { val projectionState = MediaProjectionState.Projecting.SingleTask( "host.package", + hostDeviceName = null, createTask(taskId = 1, baseIntent = baseIntent), ) @@ -92,6 +93,7 @@ class EndMediaProjectionDialogHelperTest : SysuiTestCase() { val projectionState = MediaProjectionState.Projecting.SingleTask( "host.package", + hostDeviceName = null, createTask(taskId = 1, baseIntent = baseIntent), ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt index 83b31c2a7b94..6bfb40fa17c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt @@ -103,7 +103,11 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording val task = createTask(taskId = 1) mediaProjectionRepo.mediaProjectionState.value = - MediaProjectionState.Projecting.SingleTask("host.package", task) + MediaProjectionState.Projecting.SingleTask( + "host.package", + hostDeviceName = null, + task, + ) assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index 0a06cc773727..4728c649b9a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos @@ -34,7 +33,6 @@ import com.android.systemui.screenrecord.data.repository.screenRecordRepository import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel -import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.util.time.fakeSystemClock @@ -42,9 +40,7 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before -import org.mockito.ArgumentMatchers import org.mockito.kotlin.any -import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -58,17 +54,6 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { private val systemClock = kosmos.fakeSystemClock private val mockSystemUIDialog = mock<SystemUIDialog>() - private val chipBackgroundView = mock<ChipBackgroundContainer>() - private val chipView = - mock<View>().apply { - whenever( - this.requireViewById<ChipBackgroundContainer>( - R.id.ongoing_activity_chip_background - ) - ) - .thenReturn(chipBackgroundView) - } - private val underTest = kosmos.screenRecordChipViewModel @Before @@ -197,15 +182,9 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(chipView) + clickListener!!.onClick(mock<View>()) // EndScreenRecordingDialogDelegate will test that the dialog has the right message - verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockSystemUIDialog), - eq(chipBackgroundView), - eq(null), - ArgumentMatchers.anyBoolean(), - ) + verify(mockSystemUIDialog).show() } @Test @@ -219,15 +198,9 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(chipView) + clickListener!!.onClick(mock<View>()) // EndScreenRecordingDialogDelegate will test that the dialog has the right message - verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockSystemUIDialog), - eq(chipBackgroundView), - eq(null), - ArgumentMatchers.anyBoolean(), - ) + verify(mockSystemUIDialog).show() } @Test @@ -238,20 +211,15 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( "host.package", + hostDeviceName = null, FakeActivityTaskManager.createTask(taskId = 1) ) val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(chipView) + clickListener!!.onClick(mock<View>()) // EndScreenRecordingDialogDelegate will test that the dialog has the right message - verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockSystemUIDialog), - eq(chipBackgroundView), - eq(null), - ArgumentMatchers.anyBoolean(), - ) + verify(mockSystemUIDialog).show() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt index bfb57c51206a..325a42bca7d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt @@ -116,6 +116,7 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { createAndSetDelegate( MediaProjectionState.Projecting.SingleTask( HOST_PACKAGE, + hostDeviceName = null, createTask(taskId = 1, baseIntent = baseIntent) ) ) @@ -140,6 +141,7 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { createAndSetDelegate( MediaProjectionState.Projecting.SingleTask( HOST_PACKAGE, + hostDeviceName = null, createTask(taskId = 1, baseIntent = baseIntent) ) ) @@ -200,7 +202,6 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { ProjectionChipModel.Projecting( ProjectionChipModel.Type.SHARE_TO_APP, state, - deviceName = null, ), ) } @@ -209,6 +210,10 @@ class EndShareToAppDialogDelegateTest : SysuiTestCase() { private const val HOST_PACKAGE = "fake.host.package" private val ENTIRE_SCREEN = MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE) private val SINGLE_TASK = - MediaProjectionState.Projecting.SingleTask(HOST_PACKAGE, createTask(taskId = 1)) + MediaProjectionState.Projecting.SingleTask( + HOST_PACKAGE, + hostDeviceName = null, + createTask(taskId = 1) + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index 3028d008f01d..f87b17dc92d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos @@ -35,7 +34,6 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel -import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.util.time.fakeSystemClock @@ -43,9 +41,7 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before -import org.mockito.ArgumentMatchers import org.mockito.kotlin.any -import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -59,17 +55,6 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { private val mockShareDialog = mock<SystemUIDialog>() - private val chipBackgroundView = mock<ChipBackgroundContainer>() - private val chipView = - mock<View>().apply { - whenever( - this.requireViewById<ChipBackgroundContainer>( - R.id.ongoing_activity_chip_background - ) - ) - .thenReturn(chipBackgroundView) - } - private val underTest = kosmos.shareToAppChipViewModel @Before @@ -98,6 +83,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( CAST_TO_OTHER_DEVICES_PACKAGE, + hostDeviceName = null, createTask(taskId = 1), ) @@ -123,6 +109,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( NORMAL_PACKAGE, + hostDeviceName = null, createTask(taskId = 1), ) @@ -176,6 +163,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( NORMAL_PACKAGE, + hostDeviceName = null, createTask(taskId = 1), ) @@ -193,14 +181,8 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(chipView) - verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockShareDialog), - eq(chipBackgroundView), - eq(null), - ArgumentMatchers.anyBoolean(), - ) + clickListener!!.onClick(mock<View>()) + verify(mockShareDialog).show() } @Test @@ -210,19 +192,14 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( NORMAL_PACKAGE, + hostDeviceName = null, createTask(taskId = 1), ) val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(chipView) - verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockShareDialog), - eq(chipBackgroundView), - eq(null), - ArgumentMatchers.anyBoolean(), - ) + clickListener!!.onClick(mock<View>()) + verify(mockShareDialog).show() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt index c9c7359a89eb..2e0c7735c837 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt @@ -19,50 +19,25 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.DialogTransitionAnimator -import com.android.systemui.res.R -import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener import com.android.systemui.statusbar.phone.SystemUIDialog import kotlin.test.Test -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever @SmallTest class OngoingActivityChipViewModelTest : SysuiTestCase() { private val mockSystemUIDialog = mock<SystemUIDialog>() private val dialogDelegate = SystemUIDialog.Delegate { mockSystemUIDialog } - private val dialogTransitionAnimator = mock<DialogTransitionAnimator>() - - private val chipBackgroundView = mock<ChipBackgroundContainer>() - private val chipView = - mock<View>().apply { - whenever( - this.requireViewById<ChipBackgroundContainer>( - R.id.ongoing_activity_chip_background - ) - ) - .thenReturn(chipBackgroundView) - } @Test fun createDialogLaunchOnClickListener_showsDialogOnClick() { - val clickListener = - createDialogLaunchOnClickListener(dialogDelegate, dialogTransitionAnimator) + val clickListener = createDialogLaunchOnClickListener(dialogDelegate) // Dialogs must be created on the main thread context.mainExecutor.execute { - clickListener.onClick(chipView) - verify(dialogTransitionAnimator) - .showFromView( - eq(mockSystemUIDialog), - eq(chipBackgroundView), - eq(null), - anyBoolean(), - ) + clickListener.onClick(mock<View>()) + verify(mockSystemUIDialog).show() } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index 8bc83cf2f3c2..b1a8d0beab34 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -155,6 +155,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.Projecting.SingleTask( NORMAL_PACKAGE, + hostDeviceName = null, createTask(taskId = 1), ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java index 7cb41f119c9a..10d07a0ce004 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java @@ -35,8 +35,8 @@ import android.os.Handler; import android.os.PowerManager; import android.provider.Settings; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SysuiTestCase; @@ -52,6 +52,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; import com.android.systemui.unfold.FoldAodAnimationController; import com.android.systemui.unfold.SysUIUnfoldComponent; +import com.android.systemui.util.settings.FakeSettings; import org.junit.Assert; import org.junit.Before; @@ -130,7 +131,8 @@ public class DozeParametersTest extends SysuiTestCase { mConfigurationController, mStatusBarStateController, mUserTracker, - mDozeInteractor + mDozeInteractor, + new FakeSettings() ); verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 30e96f10918d..e439aff423b0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -738,6 +738,28 @@ class MobileIconInteractorTest : SysuiTestCase() { assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java) } + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + // See b/346904529 for more context + fun satBasedIcon_doesNotInflateSignalStrength() = + testScope.runTest { + val latest by collectLastValue(underTest.signalLevelIcon) + + // GIVEN a satellite connection + connectionRepository.isNonTerrestrial.value = true + // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH + connectionRepository.inflateSignalStrength.value = true + + connectionRepository.primaryLevel.value = 4 + assertThat(latest!!.level).isEqualTo(4) + + connectionRepository.inflateSignalStrength.value = true + connectionRepository.primaryLevel.value = 4 + + // Icon level is unaffected + assertThat(latest!!.level).isEqualTo(4) + } + private fun createInteractor( overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl() ) = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index cec41557f344..e51092429cd6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -862,6 +862,38 @@ class MobileIconViewModelTest : SysuiTestCase() { assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) } + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + fun satelliteIcon_ignoresInflateSignalStrength() = + testScope.runTest { + // Note that this is the exact same test as above, but with inflateSignalStrength set to + // true we note that the level is unaffected by inflation + repository.inflateSignalStrength.value = true + repository.isNonTerrestrial.value = true + repository.setAllLevels(0) + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.setAllLevels(1) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.setAllLevels(2) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.setAllLevels(3) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.setAllLevels(4) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + private fun createAndSetViewModel() { underTest = MobileIconViewModel( 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 ef4e7341db74..cc2ef53c6cdb 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 @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.ui.viewmodel +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -32,6 +33,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.domain.interactor.keyguardStatusBarInteractor import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository +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 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -126,6 +128,7 @@ class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test + @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) fun isVisible_headsUpStatusBarShown_false() = testScope.runTest { val latest by collectLastValue(underTest.isVisible) diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt index cf0db7b51676..ce6bc09093bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt @@ -29,9 +29,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith +@Ignore @SmallTest @RunWith(AndroidJUnit4::class) class BackGestureMonitorTest : SysuiTestCase() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt index 769f264f0870..f5ef8b005e23 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt @@ -34,9 +34,11 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK import com.google.common.truth.Truth.assertThat +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith +@Ignore @SmallTest @RunWith(AndroidJUnit4::class) class TouchpadGestureHandlerTest : SysuiTestCase() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/tuner/TunablePaddingTest.java b/packages/SystemUI/tests/src/com/android/systemui/tuner/TunablePaddingTest.java deleted file mode 100644 index bb7b31b95f5e..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/tuner/TunablePaddingTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2017 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.tuner; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.testing.LeakCheck.Tracker; -import android.util.DisplayMetrics; -import android.view.View; -import android.view.WindowManager; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.utils.leaks.LeakCheckedTest; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class TunablePaddingTest extends LeakCheckedTest { - - private static final String KEY = "KEY"; - private static final int DEFAULT = 42; - private View mView; - private TunablePadding mTunablePadding; - private TunerService mTunerService; - - @Before - public void setup() { - injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES); - mView = mock(View.class); - when(mView.getContext()).thenReturn(mContext); - - mTunerService = mock(TunerService.class); - mDependency.injectTestDependency(TunablePadding.TunablePaddingService.class, - new TunablePadding.TunablePaddingService(mTunerService)); - Tracker tracker = mLeakCheck.getTracker("tuner"); - doAnswer(invocation -> { - tracker.getLeakInfo(invocation.getArguments()[0]).addAllocation(new Throwable()); - return null; - }).when(mTunerService).addTunable(any(), any()); - doAnswer(invocation -> { - tracker.getLeakInfo(invocation.getArguments()[0]).clearAllocations(); - return null; - }).when(mTunerService).removeTunable(any()); - } - - @Test - public void testFlags() { - mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT, - TunablePadding.FLAG_START); - mTunablePadding.onTuningChanged(null, null); - verify(mView).setPadding(eq(DEFAULT), eq(0), eq(0), eq(0)); - mTunablePadding.destroy(); - - mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT, - TunablePadding.FLAG_TOP); - mTunablePadding.onTuningChanged(null, null); - verify(mView).setPadding(eq(0), eq(DEFAULT), eq(0), eq(0)); - mTunablePadding.destroy(); - - mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT, - TunablePadding.FLAG_END); - mTunablePadding.onTuningChanged(null, null); - verify(mView).setPadding(eq(0), eq(0), eq(DEFAULT), eq(0)); - mTunablePadding.destroy(); - - mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT, - TunablePadding.FLAG_BOTTOM); - mTunablePadding.onTuningChanged(null, null); - verify(mView).setPadding(eq(0), eq(0), eq(0), eq(DEFAULT)); - mTunablePadding.destroy(); - } - - @Test - public void testRtl() { - when(mView.isLayoutRtl()).thenReturn(true); - - mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT, - TunablePadding.FLAG_END); - mTunablePadding.onTuningChanged(null, null); - verify(mView).setPadding(eq(DEFAULT), eq(0), eq(0), eq(0)); - mTunablePadding.destroy(); - - mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT, - TunablePadding.FLAG_START); - mTunablePadding.onTuningChanged(null, null); - verify(mView).setPadding(eq(0), eq(0), eq(DEFAULT), eq(0)); - mTunablePadding.destroy(); - } - - @Test - public void testTuning() { - int value = 3; - mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT, - TunablePadding.FLAG_START); - mTunablePadding.onTuningChanged(KEY, String.valueOf(value)); - - DisplayMetrics metrics = new DisplayMetrics(); - mContext.getSystemService(WindowManager.class).getDefaultDisplay().getMetrics(metrics); - int output = (int) (metrics.density * value); - verify(mView).setPadding(eq(output), eq(0), eq(0), eq(0)); - - mTunablePadding.destroy(); - } -} 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 index dd791e764e01..5ac61102fa99 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt @@ -28,9 +28,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.launch +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Before @@ -44,6 +45,7 @@ import org.mockito.kotlin.eq @RunWith(AndroidJUnit4::class) @SmallTest @TestableLooper.RunWithLooper +@OptIn(ExperimentalCoroutinesApi::class) class SettingsProxyTest : SysuiTestCase() { private val testDispatcher = StandardTestDispatcher() @@ -60,11 +62,12 @@ class SettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserver_inputString_success() { - mSettings.registerContentObserverSync(TEST_SETTING, mContentObserver) - verify(mSettings.getContentResolver()) - .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver)) - } + fun registerContentObserver_inputString_success() = + testScope.runTest { + mSettings.registerContentObserverSync(TEST_SETTING, mContentObserver) + verify(mSettings.getContentResolver()) + .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver)) + } @Test fun registerContentObserverSuspend_inputString_success() = @@ -75,24 +78,25 @@ class SettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverAsync_inputString_success() { - mSettings.registerContentObserverAsync(TEST_SETTING, mContentObserver) - testScope.launch { + fun registerContentObserverAsync_inputString_success() = + testScope.runTest { + mSettings.registerContentObserverAsync(TEST_SETTING, mContentObserver) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()) .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver)) } - } @Test - fun registerContentObserver_inputString_notifyForDescendants_true() { - mSettings.registerContentObserverSync( - TEST_SETTING, - notifyForDescendants = true, - mContentObserver - ) - verify(mSettings.getContentResolver()) - .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver)) - } + fun registerContentObserver_inputString_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverSync( + TEST_SETTING, + notifyForDescendants = true, + mContentObserver + ) + verify(mSettings.getContentResolver()) + .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver)) + } @Test fun registerContentObserverSuspend_inputString_notifyForDescendants_true() = @@ -107,24 +111,25 @@ class SettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverAsync_inputString_notifyForDescendants_true() { - mSettings.registerContentObserverAsync( - TEST_SETTING, - notifyForDescendants = true, - mContentObserver - ) - testScope.launch { + fun registerContentObserverAsync_inputString_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverAsync( + TEST_SETTING, + notifyForDescendants = true, + mContentObserver + ) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()) .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver)) } - } @Test - fun registerContentObserver_inputUri_success() { - mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver) - verify(mSettings.getContentResolver()) - .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver)) - } + fun registerContentObserver_inputUri_success() = + testScope.runTest { + mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver) + verify(mSettings.getContentResolver()) + .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver)) + } @Test fun registerContentObserverSuspend_inputUri_success() = @@ -135,24 +140,25 @@ class SettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverAsync_inputUri_success() { - mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver) - testScope.launch { + fun registerContentObserverAsync_inputUri_success() = + testScope.runTest { + mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()) .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver)) } - } @Test - fun registerContentObserver_inputUri_notifyForDescendants_true() { - mSettings.registerContentObserverSync( - TEST_SETTING_URI, - notifyForDescendants = true, - mContentObserver - ) - verify(mSettings.getContentResolver()) - .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver)) - } + fun registerContentObserver_inputUri_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverSync( + TEST_SETTING_URI, + notifyForDescendants = true, + mContentObserver + ) + verify(mSettings.getContentResolver()) + .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver)) + } @Test fun registerContentObserverSuspend_inputUri_notifyForDescendants_true() = @@ -167,25 +173,58 @@ class SettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverAsync_inputUri_notifyForDescendants_true() { - mSettings.registerContentObserverAsync( - TEST_SETTING_URI, - notifyForDescendants = true, - mContentObserver - ) - testScope.launch { + fun registerContentObserverAsync_inputUri_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverAsync( + TEST_SETTING_URI, + notifyForDescendants = true, + mContentObserver + ) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()) .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver)) } - } @Test - fun unregisterContentObserverSync() { - mSettings.unregisterContentObserverSync(mContentObserver) - verify(mSettings.getContentResolver()).unregisterContentObserver(eq(mContentObserver)) + fun registerContentObserverAsync_registeredLambdaPassed_callsCallback() = + testScope.runTest { + verifyRegisteredCallbackForRegistration { + mSettings.registerContentObserverAsync(TEST_SETTING, mContentObserver, it) + } + verifyRegisteredCallbackForRegistration { + mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver, it) + } + verifyRegisteredCallbackForRegistration { + mSettings.registerContentObserverAsync(TEST_SETTING, false, mContentObserver, it) + } + verifyRegisteredCallbackForRegistration { + mSettings.registerContentObserverAsync( + TEST_SETTING_URI, + false, + mContentObserver, + it + ) + } + } + + private fun verifyRegisteredCallbackForRegistration( + call: (registeredRunnable: Runnable) -> Unit + ) { + var callbackCalled = false + val runnable = { callbackCalled = true } + call(runnable) + testScope.advanceUntilIdle() + assertThat(callbackCalled).isTrue() } @Test + fun unregisterContentObserverSync() = + testScope.runTest { + mSettings.unregisterContentObserverSync(mContentObserver) + verify(mSettings.getContentResolver()).unregisterContentObserver(eq(mContentObserver)) + } + + @Test fun unregisterContentObserverSuspend_inputString_success() = testScope.runTest { mSettings.unregisterContentObserver(mContentObserver) @@ -193,12 +232,12 @@ class SettingsProxyTest : SysuiTestCase() { } @Test - fun unregisterContentObserverAsync_inputString_success() { - mSettings.unregisterContentObserverAsync(mContentObserver) - testScope.launch { + fun unregisterContentObserverAsync_inputString_success() = + testScope.runTest { + mSettings.unregisterContentObserverAsync(mContentObserver) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()).unregisterContentObserver(eq(mContentObserver)) } - } @Test fun getString_keyPresent_returnValidValue() { 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 index e3e20c8ed501..5f7420d5a16b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt @@ -31,9 +31,11 @@ import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserTracker import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Before @@ -65,20 +67,21 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverForUser_inputString_success() { - mSettings.registerContentObserverForUserSync( - TEST_SETTING, - mContentObserver, - mUserTracker.userId - ) - verify(mSettings.getContentResolver()) - .registerContentObserver( - eq(TEST_SETTING_URI), - eq(false), - eq(mContentObserver), - eq(MAIN_USER_ID) + fun registerContentObserverForUser_inputString_success() = + testScope.runTest { + mSettings.registerContentObserverForUserSync( + TEST_SETTING, + mContentObserver, + mUserTracker.userId ) - } + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(false), + eq(mContentObserver), + eq(MAIN_USER_ID) + ) + } @Test fun registerContentObserverForUserSuspend_inputString_success() = @@ -98,13 +101,14 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverForUserAsync_inputString_success() { - mSettings.registerContentObserverForUserAsync( - TEST_SETTING, - mContentObserver, - mUserTracker.userId - ) - testScope.launch { + fun registerContentObserverForUserAsync_inputString_success() = + testScope.runTest { + mSettings.registerContentObserverForUserAsync( + TEST_SETTING, + mContentObserver, + mUserTracker.userId + ) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), @@ -113,24 +117,24 @@ class UserSettingsProxyTest : SysuiTestCase() { eq(MAIN_USER_ID) ) } - } @Test - fun registerContentObserverForUser_inputString_notifyForDescendants_true() { - mSettings.registerContentObserverForUserSync( - TEST_SETTING, - notifyForDescendants = true, - mContentObserver, - mUserTracker.userId - ) - verify(mSettings.getContentResolver()) - .registerContentObserver( - eq(TEST_SETTING_URI), - eq(true), - eq(mContentObserver), - eq(MAIN_USER_ID) + fun registerContentObserverForUser_inputString_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverForUserSync( + 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 registerContentObserverForUserSuspend_inputString_notifyForDescendants_true() = @@ -153,14 +157,15 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverForUserAsync_inputString_notifyForDescendants_true() { - mSettings.registerContentObserverForUserAsync( - TEST_SETTING, - notifyForDescendants = true, - mContentObserver, - mUserTracker.userId - ) - testScope.launch { + fun registerContentObserverForUserAsync_inputString_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverForUserAsync( + TEST_SETTING, + notifyForDescendants = true, + mContentObserver, + mUserTracker.userId + ) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), @@ -169,23 +174,23 @@ class UserSettingsProxyTest : SysuiTestCase() { eq(MAIN_USER_ID) ) } - } @Test - fun registerContentObserverForUser_inputUri_success() { - mSettings.registerContentObserverForUserSync( - TEST_SETTING_URI, - mContentObserver, - mUserTracker.userId - ) - verify(mSettings.getContentResolver()) - .registerContentObserver( - eq(TEST_SETTING_URI), - eq(false), - eq(mContentObserver), - eq(MAIN_USER_ID) + fun registerContentObserverForUser_inputUri_success() = + testScope.runTest { + mSettings.registerContentObserverForUserSync( + TEST_SETTING_URI, + mContentObserver, + mUserTracker.userId ) - } + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(false), + eq(mContentObserver), + eq(MAIN_USER_ID) + ) + } @Test fun registerContentObserverForUserSuspend_inputUri_success() = @@ -205,13 +210,15 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverForUserAsync_inputUri_success() { - mSettings.registerContentObserverForUserAsync( - TEST_SETTING_URI, - mContentObserver, - mUserTracker.userId - ) - testScope.launch { + fun registerContentObserverForUserAsync_inputUri_success() = + testScope.runTest { + mSettings.registerContentObserverForUserAsync( + TEST_SETTING_URI, + mContentObserver, + mUserTracker.userId + ) + testScope.advanceUntilIdle() + verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), @@ -220,24 +227,41 @@ class UserSettingsProxyTest : SysuiTestCase() { eq(MAIN_USER_ID) ) } - } + @OptIn(ExperimentalCoroutinesApi::class) @Test - fun registerContentObserverForUser_inputUri_notifyForDescendants_true() { - mSettings.registerContentObserverForUserSync( - TEST_SETTING_URI, - notifyForDescendants = true, - mContentObserver, - mUserTracker.userId - ) - verify(mSettings.getContentResolver()) - .registerContentObserver( - eq(TEST_SETTING_URI), - eq(true), - eq(mContentObserver), - eq(MAIN_USER_ID) + fun registerContentObserverForUserAsync_callbackAfterRegister() = + testScope.runTest { + var callbackCalled = false + val runnable = { callbackCalled = true } + + mSettings.registerContentObserverForUserAsync( + TEST_SETTING_URI, + mContentObserver, + mUserTracker.userId, + runnable ) - } + testScope.advanceUntilIdle() + assertThat(callbackCalled).isTrue() + } + + @Test + fun registerContentObserverForUser_inputUri_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverForUserSync( + 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 registerContentObserverForUserSuspend_inputUri_notifyForDescendants_true() = @@ -260,14 +284,15 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserverForUserAsync_inputUri_notifyForDescendants_true() { - mSettings.registerContentObserverForUserAsync( - TEST_SETTING_URI, - notifyForDescendants = true, - mContentObserver, - mUserTracker.userId - ) - testScope.launch { + fun registerContentObserverForUserAsync_inputUri_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverForUserAsync( + TEST_SETTING_URI, + notifyForDescendants = true, + mContentObserver, + mUserTracker.userId + ) + testScope.advanceUntilIdle() verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), @@ -276,14 +301,19 @@ class UserSettingsProxyTest : SysuiTestCase() { eq(MAIN_USER_ID) ) } - } @Test - fun registerContentObserver_inputUri_success() { - mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver) - verify(mSettings.getContentResolver()) - .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver), eq(0)) - } + fun registerContentObserver_inputUri_success() = + testScope.runTest { + mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver) + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(false), + eq(mContentObserver), + eq(0) + ) + } @Test fun registerContentObserverSuspend_inputUri_success() = @@ -313,33 +343,26 @@ class UserSettingsProxyTest : SysuiTestCase() { } @Test - fun registerContentObserver_inputUri_notifyForDescendants_true() { - mSettings.registerContentObserverSync( - TEST_SETTING_URI, - notifyForDescendants = true, - mContentObserver - ) - verify(mSettings.getContentResolver()) - .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver), eq(0)) - } - - @Test - fun registerContentObserverSuspend_inputUri_notifyForDescendants_true() = + fun registerContentObserver_inputUri_notifyForDescendants_true() = testScope.runTest { - mSettings.registerContentObserver(TEST_SETTING_URI, mContentObserver) + mSettings.registerContentObserverSync( + TEST_SETTING_URI, + notifyForDescendants = true, + mContentObserver + ) verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), - eq(false), + eq(true), eq(mContentObserver), eq(0) ) } @Test - fun registerContentObserverAsync_inputUri_notifyForDescendants_true() { - mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver) - testScope.launch { + fun registerContentObserverSuspend_inputUri_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserver(TEST_SETTING_URI, mContentObserver) verify(mSettings.getContentResolver()) .registerContentObserver( eq(TEST_SETTING_URI), @@ -348,7 +371,21 @@ class UserSettingsProxyTest : SysuiTestCase() { eq(0) ) } - } + + @Test + fun registerContentObserverAsync_inputUri_notifyForDescendants_true() = + testScope.runTest { + mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver) + testScope.launch { + verify(mSettings.getContentResolver()) + .registerContentObserver( + eq(TEST_SETTING_URI), + eq(false), + eq(mContentObserver), + eq(0) + ) + } + } @Test fun getString_keyPresent_returnValidValue() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java index c81623e627ae..49aedccde258 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java @@ -19,28 +19,46 @@ package com.android.systemui.volume; import static android.media.AudioManager.CSD_WARNING_DOSE_REACHED_1X; import static android.media.AudioManager.CSD_WARNING_DOSE_REPEATED_5X; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import android.app.Notification; import android.app.NotificationManager; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.media.AudioManager; +import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; +import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.internal.messages.nano.SystemMessageProto; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; +import com.google.common.collect.ImmutableList; + +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.List; +import java.util.Optional; + @SmallTest @RunWith(AndroidJUnit4.class) @TestableLooper.RunWithLooper @@ -48,41 +66,109 @@ public class CsdWarningDialogTest extends SysuiTestCase { private NotificationManager mNotificationManager; private AudioManager mAudioManager; + private BroadcastDispatcher mFakeBroadcastDispatcher; + private CsdWarningDialog mDialog; + private static final String DISMISS_CSD_NOTIFICATION = + "com.android.systemui.volume.DISMISS_CSD_NOTIFICATION"; @Before public void setup() { mNotificationManager = mock(NotificationManager.class); - getContext().addMockSystemService(NotificationManager.class, mNotificationManager); + mContext.addMockSystemService(NotificationManager.class, mNotificationManager); mAudioManager = mock(AudioManager.class); - getContext().addMockSystemService(AudioManager.class, mAudioManager); + mContext.addMockSystemService(AudioManager.class, mAudioManager); + mFakeBroadcastDispatcher = getFakeBroadcastDispatcher(); } @Test public void create1XCsdDialogAndWait_sendsNotification() { FakeExecutor executor = new FakeExecutor(new FakeSystemClock()); // instantiate directly instead of via factory; we don't want executor to be @Background - CsdWarningDialog dialog = new CsdWarningDialog(CSD_WARNING_DOSE_REACHED_1X, mContext, - mAudioManager, mNotificationManager, executor, null); + mDialog = new CsdWarningDialog(CSD_WARNING_DOSE_REACHED_1X, mContext, + mAudioManager, mNotificationManager, executor, null, + Optional.of(ImmutableList.of(new Pair("", new Intent()))), + mFakeBroadcastDispatcher); - dialog.show(); + mDialog.show(); executor.advanceClockToLast(); executor.runAllReady(); - dialog.dismiss(); + mDialog.dismiss(); verify(mNotificationManager).notify( eq(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO), any(Notification.class)); } @Test - public void create5XCsdDiSalogAndWait_willSendNotification() { + public void create5XCsdDialogAndWait_willSendNotification() { FakeExecutor executor = new FakeExecutor(new FakeSystemClock()); - CsdWarningDialog dialog = new CsdWarningDialog(CSD_WARNING_DOSE_REPEATED_5X, mContext, - mAudioManager, mNotificationManager, executor, null); + mDialog = new CsdWarningDialog(CSD_WARNING_DOSE_REPEATED_5X, mContext, + mAudioManager, mNotificationManager, executor, null, + Optional.of(ImmutableList.of(new Pair("", new Intent()))), + mFakeBroadcastDispatcher); - dialog.show(); + mDialog.show(); verify(mNotificationManager).notify( eq(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO), any(Notification.class)); } + + @Test + @EnableFlags(Flags.FLAG_SOUNDDOSE_CUSTOMIZATION) + public void create1XCsdDialogWithActionsAndUndoIntent_willRegisterReceiverAndUndoVolume() { + FakeExecutor executor = new FakeExecutor(new FakeSystemClock()); + Intent undoIntent = new Intent(VolumeDialog.ACTION_VOLUME_UNDO) + .setPackage(mContext.getPackageName()); + mDialog = new CsdWarningDialog(CSD_WARNING_DOSE_REPEATED_5X, mContext, + mAudioManager, mNotificationManager, executor, null, + Optional.of(ImmutableList.of(new Pair("Undo", undoIntent))), + mFakeBroadcastDispatcher); + + when(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(25); + mDialog.show(); + executor.advanceClockToLast(); + executor.runAllReady(); + mDialog.dismiss(); + mDialog.mReceiverUndo.onReceive(mContext, undoIntent); + + verify(mNotificationManager).notify( + eq(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO), + any(Notification.class)); + verify(mAudioManager).setStreamVolume( + eq(AudioManager.STREAM_MUSIC), + eq(25), + eq(AudioManager.FLAG_SHOW_UI)); + } + + @Test + @EnableFlags(Flags.FLAG_SOUNDDOSE_CUSTOMIZATION) + public void deleteNotificationIntent_willUnregisterAllReceivers() { + FakeExecutor executor = new FakeExecutor(new FakeSystemClock()); + Intent undoIntent = new Intent(VolumeDialog.ACTION_VOLUME_UNDO) + .setPackage(mContext.getPackageName()); + mDialog = new CsdWarningDialog(CSD_WARNING_DOSE_REPEATED_5X, mContext, + mAudioManager, mNotificationManager, executor, null, + Optional.of(ImmutableList.of(new Pair("Undo", undoIntent))), + mFakeBroadcastDispatcher); + Intent dismissIntent = new Intent(DISMISS_CSD_NOTIFICATION) + .setPackage(mContext.getPackageName()); + + mDialog.mReceiverDismissNotification.onReceive(mContext, dismissIntent); + mDialog.show(); + executor.advanceClockToLast(); + executor.runAllReady(); + mDialog.dismiss(); + + List<ResolveInfo> resolveInfoListDismiss = mContext.getPackageManager() + .queryBroadcastReceivers(dismissIntent, PackageManager.GET_RESOLVED_FILTER); + assertThat(resolveInfoListDismiss).hasSize(0); + List<ResolveInfo> resolveInfoListUndo = mContext.getPackageManager() + .queryBroadcastReceivers(undoIntent, PackageManager.GET_RESOLVED_FILTER); + assertThat(resolveInfoListUndo).hasSize(0); + } + + @After + public void tearDown() { + mDialog.destroy(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 6efb7d8fc3bb..cdfcca6c7065 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.KeyguardManager; +import android.content.Intent; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -55,6 +56,7 @@ import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.testing.TestableLooper; import android.util.Log; +import android.util.Pair; import android.view.Gravity; import android.view.InputDevice; import android.view.MotionEvent; @@ -92,6 +94,8 @@ import com.android.systemui.volume.domain.interactor.VolumePanelNavigationIntera import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag; import com.android.systemui.volume.ui.navigation.VolumeNavigator; +import com.google.common.collect.ImmutableList; + import dagger.Lazy; import junit.framework.Assert; @@ -107,6 +111,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Arrays; +import java.util.Optional; import java.util.function.Predicate; @SmallTest @@ -157,11 +162,12 @@ public class VolumeDialogImplTest extends SysuiTestCase { private final CsdWarningDialog.Factory mCsdWarningDialogFactory = new CsdWarningDialog.Factory() { - @Override - public CsdWarningDialog create(int warningType, Runnable onCleanup) { - return mCsdWarningDialog; - } - }; + @Override + public CsdWarningDialog create(int warningType, Runnable onCleanup, + Optional<ImmutableList<Pair<String, Intent>>> actionIntents) { + return mCsdWarningDialog; + } + }; @Mock private VibratorHelper mVibratorHelper; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/android/hardware/display/DisplayManagerKosmos.kt index 0e4c923a3078..796ec9400249 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/android/hardware/display/DisplayManagerKosmos.kt @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.systemui.smartspace.data.repository +package android.hardware.display import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.util.mockito.mock -val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() } - -val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository } +val Kosmos.displayManager by Kosmos.Fixture { mock<DisplayManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt index 7c53639a85a6..0f8833cfe9f7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt @@ -16,6 +16,7 @@ package com.android.systemui import android.app.ActivityManager +import android.app.DreamManager import android.app.admin.DevicePolicyManager import android.app.trust.TrustManager import android.hardware.fingerprint.FingerprintManager @@ -33,6 +34,7 @@ import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.biometrics.AuthController import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.demomode.DemoModeController import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.ScreenLifecycle @@ -94,6 +96,7 @@ data class TestMocksModule( @get:Provides val demoModeController: DemoModeController = mock(), @get:Provides val deviceProvisionedController: DeviceProvisionedController = mock(), @get:Provides val dozeParameters: DozeParameters = mock(), + @get:Provides val dreamManager: DreamManager = mock(), @get:Provides val dumpManager: DumpManager = mock(), @get:Provides val fingerprintManager: FingerprintManager = mock(), @get:Provides val headsUpManager: HeadsUpManager = mock(), @@ -132,6 +135,7 @@ data class TestMocksModule( @get:Provides val systemUIDialogManager: SystemUIDialogManager = mock(), @get:Provides val deviceEntryIconTransitions: Set<DeviceEntryIconTransition> = emptySet(), @get:Provides val communalInteractor: CommunalInteractor = mock(), + @get:Provides val communalSceneInteractor: CommunalSceneInteractor = mock(), @get:Provides val sceneLogger: SceneLogger = mock(), @get:Provides val trustManager: TrustManager = mock(), @get:Provides val primaryBouncerInteractor: PrimaryBouncerInteractor = mock(), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt index 8b0affe2d99d..e02042d26d45 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt @@ -16,6 +16,8 @@ package com.android.systemui.accessibility +import android.content.res.Resources +import com.android.server.display.feature.flags.Flags import com.android.systemui.qs.ReduceBrightColorsController class FakeReduceBrightColorsController : ReduceBrightColorsController { @@ -44,4 +46,20 @@ class FakeReduceBrightColorsController : ReduceBrightColorsController { } } } + + override fun setReduceBrightColorsFeatureAvailable(enabled: Boolean) { + // do nothing + } + + override fun isReduceBrightColorsFeatureAvailable(): Boolean { + return true + } + + override fun isInUpgradeMode(resources: Resources?): Boolean { + if (resources != null) { + return Flags.evenDimmer() && + resources.getBoolean(com.android.internal.R.bool.config_evenDimmerEnabled) + } + return false + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryKosmos.kt new file mode 100644 index 000000000000..559a6eeb814a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeCommunalSmartspaceRepository by Kosmos.Fixture { FakeCommunalSmartspaceRepository() } + +val Kosmos.communalSmartspaceRepository by + Kosmos.Fixture<CommunalSmartspaceRepository> { fakeCommunalSmartspaceRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt index 1884a3264ed6..14b1984f2374 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt @@ -36,4 +36,18 @@ class FakeCommunalMediaRepository : CommunalMediaRepository { fun mediaInactive() { _mediaModel.value = CommunalMediaModel.INACTIVE } + + private var isListening = false + + override fun startListening() { + isListening = true + } + + override fun stopListening() { + isListening = false + } + + fun isListening(): Boolean { + return isListening + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSmartspaceRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSmartspaceRepository.kt new file mode 100644 index 000000000000..904ab4bc3f85 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSmartspaceRepository.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import com.android.systemui.communal.data.model.CommunalSmartspaceTimer +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeCommunalSmartspaceRepository : CommunalSmartspaceRepository { + + private val _timers = MutableStateFlow<List<CommunalSmartspaceTimer>>(emptyList()) + override val timers: Flow<List<CommunalSmartspaceTimer>> = _timers + + fun setTimers(timers: List<CommunalSmartspaceTimer>) { + _timers.value = timers + } + + private var isListening = false + + override fun startListening() { + isListening = true + } + + override fun stopListening() { + isListening = false + } + + fun isListening(): Boolean { + return isListening + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index b58861b1104e..eb9278537db5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.communal.domain.interactor import android.os.userManager import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.communal.data.repository.communalMediaRepository +import com.android.systemui.communal.data.repository.communalSmartspaceRepository import com.android.systemui.communal.data.repository.communalWidgetRepository import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.flags.Flags @@ -34,7 +35,6 @@ import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.settings.userTracker -import com.android.systemui.smartspace.data.repository.smartspaceRepository import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock @@ -47,7 +47,7 @@ val Kosmos.communalInteractor by Fixture { widgetRepository = communalWidgetRepository, communalPrefsInteractor = communalPrefsInteractor, mediaRepository = communalMediaRepository, - smartspaceRepository = smartspaceRepository, + smartspaceRepository = communalSmartspaceRepository, keyguardInteractor = keyguardInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, communalSettingsInteractor = communalSettingsInteractor, @@ -64,13 +64,17 @@ val Kosmos.communalInteractor by Fixture { val Kosmos.editWidgetsActivityStarter by Fixture<EditWidgetsActivityStarter> { mock() } -suspend fun Kosmos.setCommunalAvailable(available: Boolean) { - fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, available) - if (available) { +suspend fun Kosmos.setCommunalEnabled(enabled: Boolean) { + fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, enabled) + if (enabled) { fakeUserRepository.asMainUser() } else { fakeUserRepository.asDefaultUser() } +} + +suspend fun Kosmos.setCommunalAvailable(available: Boolean) { + setCommunalEnabled(available) with(fakeKeyguardRepository) { setIsEncryptedOrLockdown(!available) setKeyguardShowing(available) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt index eff99e047f45..28355e1c3efa 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt @@ -16,9 +16,10 @@ package com.android.systemui.haptics.qs +import com.android.systemui.classifier.falsingManager import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.policy.keyguardStateController val Kosmos.qsLongPressEffect by - Kosmos.Fixture { QSLongPressEffect(vibratorHelper, keyguardStateController) } + Kosmos.Fixture { QSLongPressEffect(vibratorHelper, keyguardStateController, falsingManager) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index f436a68aa5be..001b55b99919 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -25,6 +25,7 @@ import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCate import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource +import com.android.systemui.keyboard.shortcut.data.source.CurrentAppShortcutsSource import com.android.systemui.keyboard.shortcut.data.source.InputShortcutsSource import com.android.systemui.keyboard.shortcut.data.source.KeyboardShortcutGroupsSource import com.android.systemui.keyboard.shortcut.data.source.MultitaskingShortcutsSource @@ -67,18 +68,23 @@ val Kosmos.shortcutHelperStateRepository by ) } -val Kosmos.shortcutHelperInputShortcutsSource by +var Kosmos.shortcutHelperInputShortcutsSource: KeyboardShortcutGroupsSource by Kosmos.Fixture { InputShortcutsSource(mainResources, windowManager) } +var Kosmos.shortcutHelperCurrentAppShortcutsSource: KeyboardShortcutGroupsSource by + Kosmos.Fixture { CurrentAppShortcutsSource(windowManager) } + val Kosmos.shortcutHelperCategoriesRepository by Kosmos.Fixture { ShortcutHelperCategoriesRepository( applicationContext, + applicationCoroutineScope, testDispatcher, shortcutHelperSystemShortcutsSource, shortcutHelperMultiTaskingShortcutsSource, shortcutHelperAppCategoriesShortcutsSource, shortcutHelperInputShortcutsSource, + shortcutHelperCurrentAppShortcutsSource, fakeInputManager.inputManager, shortcutHelperStateRepository, ) @@ -91,7 +97,8 @@ val Kosmos.shortcutHelperTestHelper by applicationContext, broadcastDispatcher, fakeCommandQueue, - windowManager + fakeInputManager, + windowManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt index 40510db24f47..3e09b2379ff1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.content.Intent +import android.hardware.input.FakeInputManager import android.view.KeyboardShortcutGroup import android.view.WindowManager import android.view.WindowManager.KeyboardShortcutsReceiver @@ -31,6 +32,7 @@ class ShortcutHelperTestHelper( private val context: Context, private val fakeBroadcastDispatcher: FakeBroadcastDispatcher, private val fakeCommandQueue: FakeCommandQueue, + private val fakeInputManager: FakeInputManager, windowManager: WindowManager ) { @@ -79,6 +81,7 @@ class ShortcutHelperTestHelper( } fun toggle(deviceId: Int) { + fakeInputManager.addPhysicalKeyboard(deviceId) fakeCommandQueue.doForEachCallback { it.toggleKeyboardShortcutsMenu(deviceId) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt index 446652c7c6d8..126d85890531 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt @@ -16,7 +16,9 @@ package com.android.systemui.keyguard.domain.interactor +import android.service.dream.dreamManager import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.deviceentry.data.repository.deviceEntryRepository import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos @@ -36,9 +38,11 @@ var Kosmos.fromDozingTransitionInteractor by mainDispatcher = testDispatcher, keyguardInteractor = keyguardInteractor, communalInteractor = communalInteractor, + communalSceneInteractor = communalSceneInteractor, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, deviceEntryRepository = deviceEntryRepository, wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, + dreamManager = dreamManager ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt index 4328ca153374..406b5cb11bb4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt @@ -16,11 +16,9 @@ package com.android.systemui.keyguard.domain.interactor -import android.content.applicationContext import com.android.systemui.biometrics.domain.interactor.fingerprintPropertyInteractor import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository -import com.android.systemui.keyguard.data.repository.keyguardClockSection import com.android.systemui.keyguard.data.repository.keyguardSmartspaceSection import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -31,12 +29,9 @@ val Kosmos.keyguardBlueprintInteractor by KeyguardBlueprintInteractor( keyguardBlueprintRepository = keyguardBlueprintRepository, applicationScope = applicationCoroutineScope, - context = applicationContext, shadeInteractor = shadeInteractor, - clockInteractor = keyguardClockInteractor, configurationInteractor = configurationInteractor, fingerprintPropertyInteractor = fingerprintPropertyInteractor, - clockSection = keyguardClockSection, smartspaceSection = keyguardSmartspaceSection, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt index f253e949375e..81ba77a341b7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.mediaprojection.data.repository +import android.hardware.display.displayManager import android.os.Handler import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -30,6 +31,7 @@ val Kosmos.realMediaProjectionRepository by Kosmos.Fixture { MediaProjectionManagerRepository( mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, + displayManager = displayManager, handler = Handler.getMain(), applicationScope = applicationCoroutineScope, tasksRepository = activityTaskManagerTasksRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt index b9918f1e46d8..4660337c0d4b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt @@ -18,7 +18,6 @@ package com.android.systemui.shade.data.repository import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.shade.shared.model.ShadeMode import dagger.Binds import dagger.Module import javax.inject.Inject @@ -30,53 +29,65 @@ import kotlinx.coroutines.flow.asStateFlow @SysUISingleton class FakeShadeRepository @Inject constructor() : ShadeRepository { private val _qsExpansion = MutableStateFlow(0f) - @Deprecated("Use ShadeInteractor.qsExpansion instead") override val qsExpansion = _qsExpansion + + @Deprecated("Use ShadeInteractor.qsExpansion instead") + override val qsExpansion = _qsExpansion.asStateFlow() private val _udfpsTransitionToFullShadeProgress = MutableStateFlow(0f) - override val udfpsTransitionToFullShadeProgress = _udfpsTransitionToFullShadeProgress + override val udfpsTransitionToFullShadeProgress = + _udfpsTransitionToFullShadeProgress.asStateFlow() private val _currentFling: MutableStateFlow<FlingInfo?> = MutableStateFlow(null) - override val currentFling: StateFlow<FlingInfo?> = _currentFling + override val currentFling: StateFlow<FlingInfo?> = _currentFling.asStateFlow() private val _lockscreenShadeExpansion = MutableStateFlow(0f) - override val lockscreenShadeExpansion = _lockscreenShadeExpansion + override val lockscreenShadeExpansion = _lockscreenShadeExpansion.asStateFlow() private val _legacyShadeExpansion = MutableStateFlow(0f) + @Deprecated("Use ShadeInteractor instead") - override val legacyShadeExpansion = _legacyShadeExpansion + override val legacyShadeExpansion = _legacyShadeExpansion.asStateFlow() private val _legacyShadeTracking = MutableStateFlow(false) + @Deprecated("Use ShadeInteractor instead") - override val legacyShadeTracking = _legacyShadeTracking + override val legacyShadeTracking = _legacyShadeTracking.asStateFlow() private val _legacyQsTracking = MutableStateFlow(false) - @Deprecated("Use ShadeInteractor instead") override val legacyQsTracking = _legacyQsTracking + + @Deprecated("Use ShadeInteractor instead") + override val legacyQsTracking = _legacyQsTracking.asStateFlow() private val _legacyExpandedOrAwaitingInputTransfer = MutableStateFlow(false) + @Deprecated("Use ShadeInteractor instead") - override val legacyExpandedOrAwaitingInputTransfer = _legacyExpandedOrAwaitingInputTransfer + override val legacyExpandedOrAwaitingInputTransfer = + _legacyExpandedOrAwaitingInputTransfer.asStateFlow() private val _legacyIsQsExpanded = MutableStateFlow(false) - @Deprecated("Use ShadeInteractor instead") override val legacyIsQsExpanded = _legacyIsQsExpanded + + @Deprecated("Use ShadeInteractor instead") + override val legacyIsQsExpanded = _legacyIsQsExpanded.asStateFlow() @Deprecated("Use ShadeInteractor.isUserInteractingWithShade instead") override val legacyLockscreenShadeTracking = MutableStateFlow(false) - private val _shadeMode = MutableStateFlow<ShadeMode>(ShadeMode.Single) - override val shadeMode: StateFlow<ShadeMode> = _shadeMode.asStateFlow() - private var _isDualShadeAlignedToBottom = false override val isDualShadeAlignedToBottom get() = _isDualShadeAlignedToBottom + private var _isShadeLayoutWide = MutableStateFlow(false) + override val isShadeLayoutWide: StateFlow<Boolean> = _isShadeLayoutWide.asStateFlow() + @Deprecated("Use ShadeInteractor instead") override fun setLegacyIsQsExpanded(legacyIsQsExpanded: Boolean) { _legacyIsQsExpanded.value = legacyIsQsExpanded } private val _legacyExpandImmediate = MutableStateFlow(false) + @Deprecated("Use ShadeInteractor instead") - override val legacyExpandImmediate = _legacyExpandImmediate + override val legacyExpandImmediate = _legacyExpandImmediate.asStateFlow() @Deprecated("Use ShadeInteractor instead") override fun setLegacyExpandImmediate(legacyExpandImmediate: Boolean) { @@ -106,6 +117,7 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository { } private val _legacyQsFullscreen = MutableStateFlow(false) + @Deprecated("Use ShadeInteractor instead") override val legacyQsFullscreen = _legacyQsFullscreen @Deprecated("Use ShadeInteractor instead") @@ -114,6 +126,7 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository { } private val _legacyIsClosing = MutableStateFlow(false) + @Deprecated("Use ShadeInteractor instead") override val legacyIsClosing = _legacyIsClosing @Deprecated("Use ShadeInteractor instead") @@ -142,13 +155,13 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository { _legacyShadeExpansion.value = expandedFraction } - override fun setShadeMode(mode: ShadeMode) { - _shadeMode.value = mode - } - fun setDualShadeAlignedToBottom(isAlignedToBottom: Boolean) { _isDualShadeAlignedToBottom = isAlignedToBottom } + + override fun setShadeLayoutWide(isShadeLayoutWide: Boolean) { + _isShadeLayoutWide.value = isShadeLayoutWide + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index a00d2f4fc517..bfd6614a2272 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -45,7 +45,6 @@ val Kosmos.shadeInteractorSceneContainerImpl by scope = applicationCoroutineScope, sceneInteractor = sceneInteractor, sharedNotificationContainerInteractor = sharedNotificationContainerInteractor, - shadeRepository = shadeRepository, ) } val Kosmos.shadeInteractorLegacyImpl by diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/FakeSmartspaceRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/FakeSmartspaceRepository.kt deleted file mode 100644 index 862e52d7703f..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/FakeSmartspaceRepository.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.android.systemui.smartspace.data.repository - -import android.app.smartspace.SmartspaceTarget -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow - -class FakeSmartspaceRepository( - smartspaceRemoteViewsEnabled: Boolean = true, -) : SmartspaceRepository { - - override val isSmartspaceRemoteViewsEnabled = smartspaceRemoteViewsEnabled - - private val _communalSmartspaceTargets: MutableStateFlow<List<SmartspaceTarget>> = - MutableStateFlow(emptyList()) - override val communalSmartspaceTargets: Flow<List<SmartspaceTarget>> = - _communalSmartspaceTargets - - fun setCommunalSmartspaceTargets(targets: List<SmartspaceTarget>) { - _communalSmartspaceTargets.value = targets - } -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt index 144fe26ea230..2335f21b2094 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.content.applicationContext -import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.mediaRouterChipInteractor @@ -34,6 +33,5 @@ val Kosmos.castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel by mediaRouterChipInteractor = mediaRouterChipInteractor, systemClock = fakeSystemClock, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, - dialogTransitionAnimator = mockDialogTransitionAnimator, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt index 1d06947a40da..2773f825f368 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.content.applicationContext -import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper @@ -31,7 +30,6 @@ val Kosmos.screenRecordChipViewModel: ScreenRecordChipViewModel by context = applicationContext, interactor = screenRecordChipInteractor, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, - dialogTransitionAnimator = mockDialogTransitionAnimator, systemClock = fakeSystemClock, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt index 2e475a3c6885..1b3108cdb5df 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel import android.content.applicationContext -import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor @@ -32,6 +31,5 @@ val Kosmos.shareToAppChipViewModel: ShareToAppChipViewModel by mediaProjectionChipInteractor = mediaProjectionChipInteractor, systemClock = fakeSystemClock, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, - dialogTransitionAnimator = mockDialogTransitionAnimator, ) } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java index 9fc64a965f4b..099cb2894515 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -26,7 +26,6 @@ import android.annotation.MainThread; import android.annotation.NonNull; import android.content.Context; import android.graphics.Region; -import android.hardware.input.InputManager; import android.os.Looper; import android.os.PowerManager; import android.os.SystemClock; @@ -56,7 +55,6 @@ import com.android.server.policy.WindowManagerPolicy; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Objects; import java.util.StringJoiner; /** @@ -748,8 +746,6 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo if ((mEnabledFeatures & FLAG_FEATURE_MOUSE_KEYS) != 0) { mMouseKeysInterceptor = new MouseKeysInterceptor(mAms, - Objects.requireNonNull(mContext.getSystemService( - InputManager.class)), Looper.myLooper(), Display.DEFAULT_DISPLAY); addFirstEventHandler(Display.DEFAULT_DISPLAY, mMouseKeysInterceptor); diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index b061065d44a5..3706dccbb717 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -234,6 +234,8 @@ class AccessibilityUserState { mAccessibilityShortcutKeyTargets.clear(); mAccessibilityButtonTargets.clear(); mAccessibilityGestureTargets.clear(); + mAccessibilityQsTargets.clear(); + mA11yTilesInQsPanel.clear(); mTargetAssignedToAccessibilityButton = null; mIsTouchExplorationEnabled = false; mServiceHandlesDoubleTap = false; diff --git a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java index 3f0f23f4a2f9..56da231ad31a 100644 --- a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java +++ b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; -import android.hardware.input.InputManager; import android.hardware.input.VirtualMouse; import android.hardware.input.VirtualMouseButtonEvent; import android.hardware.input.VirtualMouseConfig; @@ -60,8 +59,8 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal; * In case multiple physical keyboard are connected to a device, * mouse keys of each physical keyboard will control a single (global) mouse pointer. */ -public class MouseKeysInterceptor extends BaseEventStreamTransformation implements Handler.Callback, - InputManager.InputDeviceListener { +public class MouseKeysInterceptor extends BaseEventStreamTransformation + implements Handler.Callback { private static final String LOG_TAG = "MouseKeysInterceptor"; // To enable these logs, run: 'adb shell setprop log.tag.MouseKeysInterceptor DEBUG' @@ -77,11 +76,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen private static final int INTERVAL_MILLIS = 10; private final AccessibilityManagerService mAms; - private final InputManager mInputManager; private final Handler mHandler; - private final int mDisplayId; - VirtualDeviceManager.VirtualDevice mVirtualDevice = null; private VirtualMouse mVirtualMouse = null; @@ -100,23 +96,23 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen /** Last time the key action was performed */ private long mLastTimeKeyActionPerformed = 0; - // TODO (b/346706749): This is currently using the numpad key bindings for mouse keys. - // Decide the final mouse key bindings with UX input. + /** Whether scroll toggle is on */ + private boolean mScrollToggleOn = false; + public enum MouseKeyEvent { - DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_1), - DOWN_MOVE(KeyEvent.KEYCODE_NUMPAD_2), - DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_3), - LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_4), - RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_6), - DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_7), - UP_MOVE(KeyEvent.KEYCODE_NUMPAD_8), - DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_9), - LEFT_CLICK(KeyEvent.KEYCODE_NUMPAD_5), - RIGHT_CLICK(KeyEvent.KEYCODE_NUMPAD_DOT), - HOLD(KeyEvent.KEYCODE_NUMPAD_MULTIPLY), - RELEASE(KeyEvent.KEYCODE_NUMPAD_SUBTRACT), - SCROLL_UP(KeyEvent.KEYCODE_A), - SCROLL_DOWN(KeyEvent.KEYCODE_S); + DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_7), + UP_MOVE_OR_SCROLL(KeyEvent.KEYCODE_8), + DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_9), + LEFT_MOVE(KeyEvent.KEYCODE_U), + RIGHT_MOVE(KeyEvent.KEYCODE_O), + DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_J), + DOWN_MOVE_OR_SCROLL(KeyEvent.KEYCODE_K), + DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_L), + LEFT_CLICK(KeyEvent.KEYCODE_I), + RIGHT_CLICK(KeyEvent.KEYCODE_SLASH), + HOLD(KeyEvent.KEYCODE_M), + RELEASE(KeyEvent.KEYCODE_COMMA), + SCROLL_TOGGLE(KeyEvent.KEYCODE_PERIOD); private final int mKeyCode; MouseKeyEvent(int enumValue) { @@ -149,22 +145,19 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * Construct a new MouseKeysInterceptor. * * @param service The service to notify of key events - * @param inputManager InputManager to track changes to connected input devices * @param looper Looper to use for callbacks and messages * @param displayId Display ID to send mouse events to */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) - public MouseKeysInterceptor(AccessibilityManagerService service, InputManager inputManager, - Looper looper, int displayId) { + public MouseKeysInterceptor(AccessibilityManagerService service, Looper looper, int displayId) { mAms = service; - mInputManager = inputManager; mHandler = new Handler(looper, this); - mInputManager.registerInputDeviceListener(this, mHandler); - mDisplayId = displayId; // Create the virtual mouse on a separate thread since virtual device creation // should happen on an auxiliary thread, and not from the handler's thread. + // This is because virtual device creation is a blocking operation and can cause a + // deadlock if it is called from the handler's thread. new Thread(() -> { - mVirtualMouse = createVirtualMouse(); + mVirtualMouse = createVirtualMouse(displayId); }).start(); } @@ -193,22 +186,23 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen /** * Performs a mouse scroll action based on the provided key code. + * The scroll action will only be performed if the scroll toggle is on. * This method interprets the key code as a mouse scroll and sends * the corresponding {@code VirtualMouseScrollEvent#mYAxisMovement}. * @param keyCode The key code representing the mouse scroll action. * Supported keys are: * <ul> - * <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_UP} - * <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_DOWN} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL} * </ul> */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private void performMouseScrollAction(int keyCode) { MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode); float y = switch (mouseKeyEvent) { - case SCROLL_UP -> 1.0f; - case SCROLL_DOWN -> -1.0f; + case UP_MOVE_OR_SCROLL -> 1.0f; + case DOWN_MOVE_OR_SCROLL -> -1.0f; default -> 0.0f; }; if (mVirtualMouse != null) { @@ -231,8 +225,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * @param keyCode The key code representing the mouse button action. * Supported keys are: * <ul> - * <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT_CLICK} (Primary Button) - * <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT_CLICK} (Secondary + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_CLICK} (Primary Button) + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_CLICK} (Secondary * Button) * </ul> */ @@ -264,17 +258,20 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * The method calculates the relative movement of the mouse pointer * and sends the corresponding event to the virtual mouse. * + * The UP and DOWN pointer actions will only take place for their respective keys + * if the scroll toggle is off. + * * @param keyCode The key code representing the direction or button press. * Supported keys are: * <ul> - * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_LEFT} - * <li>{@link MouseKeysInterceptor.MouseKeyEvent DOWN} - * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_RIGHT} - * <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT} - * <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT} - * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_LEFT} - * <li>{@link MouseKeysInterceptor.MouseKeyEvent UP} - * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_RIGHT} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_LEFT_MOVE} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_RIGHT_MOVE} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_MOVE} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_MOVE} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_LEFT_MOVE} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL} + * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_RIGHT_MOVE} * </ul> */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) @@ -287,8 +284,10 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); } - case DOWN_MOVE -> { - y = MOUSE_POINTER_MOVEMENT_STEP; + case DOWN_MOVE_OR_SCROLL -> { + if (!mScrollToggleOn) { + y = MOUSE_POINTER_MOVEMENT_STEP; + } } case DIAGONAL_DOWN_RIGHT_MOVE -> { x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); @@ -304,8 +303,10 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); } - case UP_MOVE -> { - y = -MOUSE_POINTER_MOVEMENT_STEP; + case UP_MOVE_OR_SCROLL -> { + if (!mScrollToggleOn) { + y = -MOUSE_POINTER_MOVEMENT_STEP; + } } case DIAGONAL_UP_RIGHT_MOVE -> { x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); @@ -333,8 +334,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen } private boolean isMouseScrollKey(int keyCode) { - return keyCode == MouseKeyEvent.SCROLL_UP.getKeyCodeValue() - || keyCode == MouseKeyEvent.SCROLL_DOWN.getKeyCodeValue(); + return keyCode == MouseKeyEvent.UP_MOVE_OR_SCROLL.getKeyCodeValue() + || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCodeValue(); } /** @@ -343,7 +344,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * @return The created VirtualMouse. */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) - private VirtualMouse createVirtualMouse() { + private VirtualMouse createVirtualMouse(int displayId) { final VirtualDeviceManagerInternal localVdm = LocalServices.getService(VirtualDeviceManagerInternal.class); mVirtualDevice = localVdm.createVirtualDevice( @@ -351,7 +352,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen VirtualMouse virtualMouse = mVirtualDevice.createVirtualMouse( new VirtualMouseConfig.Builder() .setInputDeviceName("Mouse Keys Virtual Mouse") - .setAssociatedDisplayId(mDisplayId) + .setAssociatedDisplayId(displayId) .build()); return virtualMouse; } @@ -375,42 +376,56 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen if (!isMouseKey(keyCode)) { // Pass non-mouse key events to the next handler super.onKeyEvent(event, policyFlags); - } else if (keyCode == MouseKeyEvent.HOLD.getKeyCodeValue()) { - sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY, - VirtualMouseButtonEvent.ACTION_BUTTON_PRESS); - } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCodeValue()) { - sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY, - VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE); - } else if (isDown && isMouseButtonKey(keyCode)) { - performMouseButtonAction(keyCode); - } else if (isDown && isMouseScrollKey(keyCode)) { - // If the scroll key is pressed down and no other key is active, - // set it as the active key and send a message to scroll the pointer - if (mActiveScrollKey == KEY_NOT_SET) { - mActiveScrollKey = keyCode; - mLastTimeKeyActionPerformed = event.getDownTime(); - mHandler.sendEmptyMessage(MESSAGE_SCROLL_MOUSE_POINTER); - } } else if (isDown) { - // This is a directional key. - // If the key is pressed down and no other key is active, - // set it as the active key and send a message to move the pointer - if (mActiveMoveKey == KEY_NOT_SET) { - mActiveMoveKey = keyCode; - mLastTimeKeyActionPerformed = event.getDownTime(); - mHandler.sendEmptyMessage(MESSAGE_MOVE_MOUSE_POINTER); + if (keyCode == MouseKeyEvent.SCROLL_TOGGLE.getKeyCodeValue()) { + mScrollToggleOn = !mScrollToggleOn; + if (DEBUG) { + Slog.d(LOG_TAG, "Scroll toggle " + (mScrollToggleOn ? "ON" : "OFF")); + } + } else if (keyCode == MouseKeyEvent.HOLD.getKeyCodeValue()) { + sendVirtualMouseButtonEvent( + VirtualMouseButtonEvent.BUTTON_PRIMARY, + VirtualMouseButtonEvent.ACTION_BUTTON_PRESS + ); + } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCodeValue()) { + sendVirtualMouseButtonEvent( + VirtualMouseButtonEvent.BUTTON_PRIMARY, + VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE + ); + } else if (isMouseButtonKey(keyCode)) { + performMouseButtonAction(keyCode); + } else if (mScrollToggleOn && isMouseScrollKey(keyCode)) { + // If the scroll key is pressed down and no other key is active, + // set it as the active key and send a message to scroll the pointer + if (mActiveScrollKey == KEY_NOT_SET) { + mActiveScrollKey = keyCode; + mLastTimeKeyActionPerformed = event.getDownTime(); + mHandler.sendEmptyMessage(MESSAGE_SCROLL_MOUSE_POINTER); + } + } else { + // This is a directional key. + // If the key is pressed down and no other key is active, + // set it as the active key and send a message to move the pointer + if (mActiveMoveKey == KEY_NOT_SET) { + mActiveMoveKey = keyCode; + mLastTimeKeyActionPerformed = event.getDownTime(); + mHandler.sendEmptyMessage(MESSAGE_MOVE_MOUSE_POINTER); + } } - } else if (mActiveMoveKey == keyCode) { - // If the key is released, and it is the active key, stop moving the pointer - mActiveMoveKey = KEY_NOT_SET; - mHandler.removeMessages(MESSAGE_MOVE_MOUSE_POINTER); - } else if (mActiveScrollKey == keyCode) { - // If the key is released, and it is the active key, stop scrolling the pointer - mActiveScrollKey = KEY_NOT_SET; - mHandler.removeMessages(MESSAGE_SCROLL_MOUSE_POINTER); } else { - Slog.i(LOG_TAG, "Dropping event with key code: '" + keyCode - + "', with no matching down event from deviceId = " + event.getDeviceId()); + // Up event received + if (mActiveMoveKey == keyCode) { + // If the key is released, and it is the active key, stop moving the pointer + mActiveMoveKey = KEY_NOT_SET; + mHandler.removeMessages(MESSAGE_MOVE_MOUSE_POINTER); + } else if (mActiveScrollKey == keyCode) { + // If the key is released, and it is the active key, stop scrolling the pointer + mActiveScrollKey = KEY_NOT_SET; + mHandler.removeMessages(MESSAGE_SCROLL_MOUSE_POINTER); + } else { + Slog.i(LOG_TAG, "Dropping event with key code: '" + keyCode + + "', with no matching down event from deviceId = " + event.getDeviceId()); + } } } @@ -470,14 +485,6 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen } } - @Override - public void onInputDeviceAdded(int deviceId) { - } - - @Override - public void onInputDeviceRemoved(int deviceId) { - } - @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) @Override public void onDestroy() { @@ -485,14 +492,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen mActiveMoveKey = KEY_NOT_SET; mActiveScrollKey = KEY_NOT_SET; mLastTimeKeyActionPerformed = 0; - mHandler.removeCallbacksAndMessages(null); + mHandler.removeCallbacksAndMessages(null); mVirtualDevice.close(); - mInputManager.unregisterInputDeviceListener(this); } - - @Override - public void onInputDeviceChanged(int deviceId) { - } - } diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java index 2e9a4dcb45aa..a10039f9bf6c 100644 --- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java @@ -462,7 +462,9 @@ public final class AutoFillUI { @Override public void onShown() { - mCallback.onShown(UI_TYPE_DIALOG, response.getDatasets().size()); + if (mCallback != null) { + mCallback.onShown(UI_TYPE_DIALOG, response.getDatasets().size()); + } } @Override @@ -511,7 +513,9 @@ public final class AutoFillUI { @Override public void startIntentSender(IntentSender intentSender) { - mCallback.startIntentSenderAndFinishSession(intentSender); + if (mCallback != null) { + mCallback.startIntentSenderAndFinishSession(intentSender); + } } private void log(int type) { diff --git a/services/core/Android.bp b/services/core/Android.bp index 1cd20ed0f7cd..9d4310c21cf9 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -215,6 +215,7 @@ java_library_static { "power_hint_flags_lib", "biometrics_flags_lib", "am_flags_lib", + "updates_flags_lib", "com_android_server_accessibility_flags_lib", "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", "com_android_wm_shell_flags_lib", diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index 21947bac137b..95dbaae2c43b 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -1016,6 +1016,7 @@ public class Watchdog implements Dumpable { // Trigger the kernel to dump all blocked threads, and backtraces on all CPUs to the // kernel log doSysRq('w'); + doSysRq('m'); doSysRq('l'); } diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index ac9ed0da95a5..320122390681 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -1233,6 +1233,10 @@ public class AccountManagerService obsoleteAuthType.add(type); // And delete it from the TABLE_META accountsDb.deleteMetaByAuthTypeAndUid(type, uid); + } else if (knownUid != null && knownUid != uid) { + Slog.w(TAG, "authenticator no longer exist for type " + type); + obsoleteAuthType.add(type); + accountsDb.deleteMetaByAuthTypeAndUid(type, uid); } } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 25fb729e7e7c..69ee8fc831f4 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -72,6 +72,7 @@ import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.SIGNATURE_NO_MATCH; +import static android.crashrecovery.flags.Flags.refactorCrashrecovery; import static android.net.ConnectivityManager.BLOCKED_REASON_NONE; import static android.os.FactoryTest.FACTORY_TEST_OFF; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; @@ -2075,7 +2076,8 @@ public class ActivityManagerService extends IActivityManager.Stub app.setPersistent(true); app.setPid(MY_PID); app.mState.setMaxAdj(ProcessList.SYSTEM_ADJ); - app.makeActive(mSystemThread.getApplicationThread(), mProcessStats); + app.makeActive(new ApplicationThreadDeferred(mSystemThread.getApplicationThread()), + mProcessStats); app.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_SYSTEM); addPidLocked(app); updateLruProcessLocked(app, false, null); @@ -2322,7 +2324,9 @@ public class ActivityManagerService extends IActivityManager.Stub } else if (phase == PHASE_ACTIVITY_MANAGER_READY) { mService.startBroadcastObservers(); } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { - mService.mPackageWatchdog.onPackagesReady(); + if (!refactorCrashrecovery()) { + mService.mPackageWatchdog.onPackagesReady(); + } mService.scheduleHomeTimeout(); } } @@ -4871,7 +4875,7 @@ public class ActivityManagerService extends IActivityManager.Stub // Make app active after binding application or client may be running requests (e.g // starting activities) before it is ready. synchronized (mProcLock) { - app.makeActive(thread, mProcessStats); + app.makeActive(new ApplicationThreadDeferred(thread), mProcessStats); checkTime(startTime, "attachApplicationLocked: immediately after bindApplication"); } app.setPendingFinishAttach(true); diff --git a/services/core/java/com/android/server/am/ApplicationThreadDeferred.java b/services/core/java/com/android/server/am/ApplicationThreadDeferred.java new file mode 100644 index 000000000000..b0f9b53ff525 --- /dev/null +++ b/services/core/java/com/android/server/am/ApplicationThreadDeferred.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; +import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; + +import android.annotation.IntDef; +import android.app.IApplicationThread; +import android.os.RemoteException; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; + +/** + * A subclass of {@link IApplicationThread} that defers certain binder calls while the process is + * paused (frozen). Any deferred calls are executed when the process is unpaused. In some cases, + * multiple instances of deferred calls are collapsed into a single call when the process is + * unpaused. + * + * {@hide} + */ +class ApplicationThreadDeferred extends ApplicationThreadFilter { + + static final String TAG = TAG_WITH_CLASS_NAME ? "ApplicationThreadDeferred" : TAG_AM; + + // The flag that enables the deferral behavior of this class. If the flag is disabled then + // the class behaves exactly like an ApplicationThreadFilter. + private static boolean deferBindersWhenPaused() { + return Flags.deferBindersWhenPaused(); + } + + // The list of notifications that may be deferred. + private static final int CLEAR_DNS_CACHE = 0; + private static final int UPDATE_TIME_ZONE = 1; + private static final int SCHEDULE_LOW_MEMORY = 2; + private static final int UPDATE_HTTP_PROXY = 3; + private static final int NOTIFICATION_COUNT = 4; + + @IntDef(value = { + CLEAR_DNS_CACHE, + UPDATE_TIME_ZONE, + SCHEDULE_LOW_MEMORY, + UPDATE_HTTP_PROXY + }) + @Retention(RetentionPolicy.SOURCE) + private @interface NotificationType {}; + + private final Object mLock = new Object(); + + // If this is true, notifications should be queued for later delivery. If this is false, + // notifications should be delivered immediately. + @GuardedBy("mLock") + private boolean mPaused = false; + + // An operation is a lambda that throws an exception. + private interface Operation { + void run() throws RemoteException; + } + + // The array of operations. + @GuardedBy("mLock") + private final Operation[] mOperations = new Operation[NOTIFICATION_COUNT]; + + // The array of operations that actually pending right now. + @GuardedBy("mLock") + private final boolean[] mPending = new boolean[NOTIFICATION_COUNT]; + + // When true, binder calls to paused processes will be deferred until the process is unpaused. + private final boolean mDefer; + + /** Create an instance with a base thread and a deferral enable flag. */ + @VisibleForTesting + public ApplicationThreadDeferred(IApplicationThread thread, boolean defer) { + super(thread); + + mDefer = defer; + + mOperations[CLEAR_DNS_CACHE] = () -> { super.clearDnsCache(); }; + mOperations[UPDATE_TIME_ZONE] = () -> { super.updateTimeZone(); }; + mOperations[SCHEDULE_LOW_MEMORY] = () -> { super.scheduleLowMemory(); }; + mOperations[UPDATE_HTTP_PROXY] = () -> { super.updateHttpProxy(); }; + } + + /** Create an instance with a base flag, using the system deferral enable flag. */ + public ApplicationThreadDeferred(IApplicationThread thread) { + this(thread, deferBindersWhenPaused()); + } + + /** The process is being paused. Start deferring calls. */ + void onProcessPaused() { + synchronized (mLock) { + mPaused = true; + } + } + + /** The process is no longer paused. Drain any deferred calls. */ + void onProcessUnpaused() { + synchronized (mLock) { + mPaused = false; + try { + for (int i = 0; i < mOperations.length; i++) { + if (mPending[i]) { + mOperations[i].run(); + } + } + } catch (RemoteException e) { + // Swallow the exception. The caller is not expecting it. Remote exceptions + // happen if a has process died; there is no need to report it here. + } finally { + Arrays.fill(mPending, false); + } + } + } + + /** The pause operation has been canceled. Drain any deferred calls. */ + void onProcessPausedCancelled() { + onProcessUnpaused(); + } + + /** + * If the thread is not paused, execute the operation. Otherwise, save it to the pending + * list. + */ + private void execute(@NotificationType int tag) throws RemoteException { + synchronized (mLock) { + if (mPaused && mDefer) { + mPending[tag] = true; + return; + } + } + // Outside the synchronization block to avoid contention. + mOperations[tag].run(); + } + + @Override + public void clearDnsCache() throws RemoteException { + execute(CLEAR_DNS_CACHE); + } + + @Override + public void updateTimeZone() throws RemoteException { + execute(UPDATE_TIME_ZONE); + } + + @Override + public void scheduleLowMemory() throws RemoteException { + execute(SCHEDULE_LOW_MEMORY); + } + + @Override + public void updateHttpProxy() throws RemoteException { + execute(UPDATE_HTTP_PROXY); + } +} diff --git a/services/core/java/com/android/server/am/ApplicationThreadFilter.java b/services/core/java/com/android/server/am/ApplicationThreadFilter.java new file mode 100644 index 000000000000..d049305025c2 --- /dev/null +++ b/services/core/java/com/android/server/am/ApplicationThreadFilter.java @@ -0,0 +1,603 @@ +/* + * 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.am; + + +class ApplicationThreadFilter implements android.app.IApplicationThread { + private final android.app.IApplicationThread mBase; + public ApplicationThreadFilter(android.app.IApplicationThread base) { mBase = base; } + android.app.IApplicationThread getBase() { return mBase; } + public android.os.IBinder asBinder() { + return mBase.asBinder(); + } + + @Override + public void scheduleReceiver(android.content.Intent intent, + android.content.pm.ActivityInfo info, + android.content.res.CompatibilityInfo compatInfo, + int resultCode, + String data, + android.os.Bundle extras, + boolean ordered, + boolean assumeDelivered, + int sendingUser, + int processState, + int sentFromUid, + String sentFromPackage) + throws android.os.RemoteException { + mBase.scheduleReceiver(intent, + info, + compatInfo, + resultCode, + data, + extras, + ordered, + assumeDelivered, + sendingUser, + processState, + sentFromUid, + sentFromPackage); + } + @Override + public void scheduleReceiverList(java.util.List<android.app.ReceiverInfo> info) + throws android.os.RemoteException { + mBase.scheduleReceiverList(info); + } + @Override + public void scheduleCreateService(android.os.IBinder token, + android.content.pm.ServiceInfo info, + android.content.res.CompatibilityInfo compatInfo, + int processState) + throws android.os.RemoteException { + mBase.scheduleCreateService(token, + info, + compatInfo, + processState); + } + @Override + public void scheduleStopService(android.os.IBinder token) + throws android.os.RemoteException { + mBase.scheduleStopService(token); + } + @Override + public void bindApplication(String packageName, + android.content.pm.ApplicationInfo info, + String sdkSandboxClientAppVolumeUuid, + String sdkSandboxClientAppPackage, + boolean isSdkInSandbox, + android.content.pm.ProviderInfoList providerList, + android.content.ComponentName testName, + android.app.ProfilerInfo profilerInfo, + android.os.Bundle testArguments, + android.app.IInstrumentationWatcher testWatcher, + android.app.IUiAutomationConnection uiAutomationConnection, + int debugMode, + boolean enableBinderTracking, + boolean trackAllocation, + boolean restrictedBackupMode, + boolean persistent, + android.content.res.Configuration config, + android.content.res.CompatibilityInfo compatInfo, + java.util.Map services, + android.os.Bundle coreSettings, + String buildSerial, + android.content.AutofillOptions autofillOptions, + android.content.ContentCaptureOptions contentCaptureOptions, + long[] disabledCompatChanges, + long[] loggableCompatChanges, + android.os.SharedMemory serializedSystemFontMap, + long startRequestedElapsedTime, + long startRequestedUptime) + throws android.os.RemoteException { + mBase.bindApplication(packageName, + info, + sdkSandboxClientAppVolumeUuid, + sdkSandboxClientAppPackage, + isSdkInSandbox, + providerList, + testName, + profilerInfo, + testArguments, + testWatcher, + uiAutomationConnection, + debugMode, + enableBinderTracking, + trackAllocation, + restrictedBackupMode, + persistent, + config, + compatInfo, + services, + coreSettings, + buildSerial, + autofillOptions, + contentCaptureOptions, + disabledCompatChanges, + loggableCompatChanges, + serializedSystemFontMap, + startRequestedElapsedTime, + startRequestedUptime); + } + @Override + public void runIsolatedEntryPoint(String entryPoint, + String[] entryPointArgs) + throws android.os.RemoteException { + mBase.runIsolatedEntryPoint(entryPoint, + entryPointArgs); + } + @Override + public void scheduleExit() + throws android.os.RemoteException { + mBase.scheduleExit(); + } + @Override + public void scheduleServiceArgs(android.os.IBinder token, + android.content.pm.ParceledListSlice args) + throws android.os.RemoteException { + mBase.scheduleServiceArgs(token, + args); + } + @Override + public void updateTimeZone() + throws android.os.RemoteException { + mBase.updateTimeZone(); + } + @Override + public void processInBackground() + throws android.os.RemoteException { + mBase.processInBackground(); + } + @Override + public void scheduleBindService(android.os.IBinder token, + android.content.Intent intent, + boolean rebind, + int processState, + long bindSeq) + throws android.os.RemoteException { + mBase.scheduleBindService(token, + intent, + rebind, + processState, + bindSeq); + } + @Override + public void scheduleUnbindService(android.os.IBinder token, + android.content.Intent intent) + throws android.os.RemoteException { + mBase.scheduleUnbindService(token, + intent); + } + @Override + public void dumpService(android.os.ParcelFileDescriptor fd, + android.os.IBinder servicetoken, + String[] args) + throws android.os.RemoteException { + mBase.dumpService(fd, + servicetoken, + args); + } + @Override + public void scheduleRegisteredReceiver(android.content.IIntentReceiver receiver, + android.content.Intent intent, + int resultCode, + String data, + android.os.Bundle extras, + boolean ordered, + boolean sticky, + boolean assumeDelivered, + int sendingUser, + int processState, + int sentFromUid, + String sentFromPackage) + throws android.os.RemoteException { + mBase.scheduleRegisteredReceiver(receiver, + intent, + resultCode, + data, + extras, + ordered, + sticky, + assumeDelivered, + sendingUser, + processState, + sentFromUid, + sentFromPackage); + } + @Override + public void scheduleLowMemory() + throws android.os.RemoteException { + mBase.scheduleLowMemory(); + } + @Override + public void profilerControl(boolean start, + android.app.ProfilerInfo profilerInfo, + int profileType) + throws android.os.RemoteException { + mBase.profilerControl(start, + profilerInfo, + profileType); + } + @Override + public void setSchedulingGroup(int group) + throws android.os.RemoteException { + mBase.setSchedulingGroup(group); + } + @Override + public void scheduleCreateBackupAgent(android.content.pm.ApplicationInfo app, + int backupMode, + int userId, + int operationType) + throws android.os.RemoteException { + mBase.scheduleCreateBackupAgent(app, + backupMode, + userId, + operationType); + } + @Override + public void scheduleDestroyBackupAgent(android.content.pm.ApplicationInfo app, + int userId) + throws android.os.RemoteException { + mBase.scheduleDestroyBackupAgent(app, + userId); + } + @Override + public void scheduleOnNewSceneTransitionInfo(android.os.IBinder token, + android.app.ActivityOptions.SceneTransitionInfo info) + throws android.os.RemoteException { + mBase.scheduleOnNewSceneTransitionInfo(token, + info); + } + @Override + public void scheduleSuicide() + throws android.os.RemoteException { + mBase.scheduleSuicide(); + } + @Override + public void dispatchPackageBroadcast(int cmd, + String[] packages) + throws android.os.RemoteException { + mBase.dispatchPackageBroadcast(cmd, + packages); + } + @Override + public void scheduleCrash(String msg, + int typeId, + android.os.Bundle extras) + throws android.os.RemoteException { + mBase.scheduleCrash(msg, + typeId, + extras); + } + @Override + public void dumpHeap(boolean managed, + boolean mallocInfo, + boolean runGc, + String dumpBitmaps, + String path, + android.os.ParcelFileDescriptor fd, + android.os.RemoteCallback finishCallback) + throws android.os.RemoteException { + mBase.dumpHeap(managed, + mallocInfo, + runGc, + dumpBitmaps, + path, + fd, + finishCallback); + } + @Override + public void dumpActivity(android.os.ParcelFileDescriptor fd, + android.os.IBinder servicetoken, + String prefix, + String[] args) + throws android.os.RemoteException { + mBase.dumpActivity(fd, + servicetoken, + prefix, + args); + } + @Override + public void dumpResources(android.os.ParcelFileDescriptor fd, + android.os.RemoteCallback finishCallback) + throws android.os.RemoteException { + mBase.dumpResources(fd, + finishCallback); + } + @Override + public void clearDnsCache() + throws android.os.RemoteException { + mBase.clearDnsCache(); + } + @Override + public void updateHttpProxy() + throws android.os.RemoteException { + mBase.updateHttpProxy(); + } + @Override + public void setCoreSettings(android.os.Bundle coreSettings) + throws android.os.RemoteException { + mBase.setCoreSettings(coreSettings); + } + @Override + public void updatePackageCompatibilityInfo(String pkg, + android.content.res.CompatibilityInfo info) + throws android.os.RemoteException { + mBase.updatePackageCompatibilityInfo(pkg, + info); + } + @Override + public void scheduleTrimMemory(int level) + throws android.os.RemoteException { + mBase.scheduleTrimMemory(level); + } + @Override + public void dumpMemInfo(android.os.ParcelFileDescriptor fd, + android.os.Debug.MemoryInfo mem, + boolean checkin, + boolean dumpInfo, + boolean dumpDalvik, + boolean dumpSummaryOnly, + boolean dumpUnreachable, + boolean dumpAllocatorLogs, + String[] args) + throws android.os.RemoteException { + mBase.dumpMemInfo(fd, + mem, + checkin, + dumpInfo, + dumpDalvik, + dumpSummaryOnly, + dumpUnreachable, + dumpAllocatorLogs, + args); + } + @Override + public void dumpMemInfoProto(android.os.ParcelFileDescriptor fd, + android.os.Debug.MemoryInfo mem, + boolean dumpInfo, + boolean dumpDalvik, + boolean dumpSummaryOnly, + boolean dumpUnreachable, + String[] args) + throws android.os.RemoteException { + mBase.dumpMemInfoProto(fd, + mem, + dumpInfo, + dumpDalvik, + dumpSummaryOnly, + dumpUnreachable, + args); + } + @Override + public void dumpGfxInfo(android.os.ParcelFileDescriptor fd, + String[] args) + throws android.os.RemoteException { + mBase.dumpGfxInfo(fd, + args); + } + @Override + public void dumpCacheInfo(android.os.ParcelFileDescriptor fd, + String[] args) + throws android.os.RemoteException { + mBase.dumpCacheInfo(fd, + args); + } + @Override + public void dumpProvider(android.os.ParcelFileDescriptor fd, + android.os.IBinder servicetoken, + String[] args) + throws android.os.RemoteException { + mBase.dumpProvider(fd, + servicetoken, + args); + } + @Override + public void dumpDbInfo(android.os.ParcelFileDescriptor fd, + String[] args) + throws android.os.RemoteException { + mBase.dumpDbInfo(fd, + args); + } + @Override + public void unstableProviderDied(android.os.IBinder provider) + throws android.os.RemoteException { + mBase.unstableProviderDied(provider); + } + @Override + public void requestAssistContextExtras(android.os.IBinder activityToken, + android.os.IBinder requestToken, + int requestType, + int sessionId, + int flags) + throws android.os.RemoteException { + mBase.requestAssistContextExtras(activityToken, + requestToken, + requestType, + sessionId, + flags); + } + @Override + public void scheduleTranslucentConversionComplete(android.os.IBinder token, + boolean timeout) + throws android.os.RemoteException { + mBase.scheduleTranslucentConversionComplete(token, + timeout); + } + @Override + public void setProcessState(int state) + throws android.os.RemoteException { + mBase.setProcessState(state); + } + @Override + public void scheduleInstallProvider(android.content.pm.ProviderInfo provider) + throws android.os.RemoteException { + mBase.scheduleInstallProvider(provider); + } + @Override + public void updateTimePrefs(int timeFormatPreference) + throws android.os.RemoteException { + mBase.updateTimePrefs(timeFormatPreference); + } + @Override + public void scheduleEnterAnimationComplete(android.os.IBinder token) + throws android.os.RemoteException { + mBase.scheduleEnterAnimationComplete(token); + } + @Override + public void notifyCleartextNetwork(byte[] firstPacket) + throws android.os.RemoteException { + mBase.notifyCleartextNetwork(firstPacket); + } + @Override + public void startBinderTracking() + throws android.os.RemoteException { + mBase.startBinderTracking(); + } + @Override + public void stopBinderTrackingAndDump(android.os.ParcelFileDescriptor fd) + throws android.os.RemoteException { + mBase.stopBinderTrackingAndDump(fd); + } + @Override + public void scheduleLocalVoiceInteractionStarted(android.os.IBinder token, + com.android.internal.app.IVoiceInteractor voiceInteractor) + throws android.os.RemoteException { + mBase.scheduleLocalVoiceInteractionStarted(token, + voiceInteractor); + } + @Override + public void handleTrustStorageUpdate() + throws android.os.RemoteException { + mBase.handleTrustStorageUpdate(); + } + @Override + public void attachAgent(String path) + throws android.os.RemoteException { + mBase.attachAgent(path); + } + @Override + public void attachStartupAgents(String dataDir) + throws android.os.RemoteException { + mBase.attachStartupAgents(dataDir); + } + @Override + public void scheduleApplicationInfoChanged(android.content.pm.ApplicationInfo ai) + throws android.os.RemoteException { + mBase.scheduleApplicationInfoChanged(ai); + } + @Override + public void setNetworkBlockSeq(long procStateSeq) + throws android.os.RemoteException { + mBase.setNetworkBlockSeq(procStateSeq); + } + @Override + public void scheduleTransaction(android.app.servertransaction.ClientTransaction transaction) + throws android.os.RemoteException { + mBase.scheduleTransaction(transaction); + } + @Override + public void scheduleTaskFragmentTransaction(android.window.ITaskFragmentOrganizer organizer, + android.window.TaskFragmentTransaction transaction) + throws android.os.RemoteException { + mBase.scheduleTaskFragmentTransaction(organizer, + transaction); + } + @Override + public void requestDirectActions(android.os.IBinder activityToken, + com.android.internal.app.IVoiceInteractor intractor, + android.os.RemoteCallback cancellationCallback, + android.os.RemoteCallback callback) + throws android.os.RemoteException { + mBase.requestDirectActions(activityToken, + intractor, + cancellationCallback, + callback); + } + @Override + public void performDirectAction(android.os.IBinder activityToken, + String actionId, + android.os.Bundle arguments, + android.os.RemoteCallback cancellationCallback, + android.os.RemoteCallback resultCallback) + throws android.os.RemoteException { + mBase.performDirectAction(activityToken, + actionId, + arguments, + cancellationCallback, + resultCallback); + } + @Override + public void notifyContentProviderPublishStatus(android.app.ContentProviderHolder holder, + String authorities, + int userId, + boolean published) + throws android.os.RemoteException { + mBase.notifyContentProviderPublishStatus(holder, + authorities, + userId, + published); + } + @Override + public void instrumentWithoutRestart(android.content.ComponentName instrumentationName, + android.os.Bundle instrumentationArgs, + android.app.IInstrumentationWatcher instrumentationWatcher, + android.app.IUiAutomationConnection instrumentationUiConnection, + android.content.pm.ApplicationInfo targetInfo) + throws android.os.RemoteException { + mBase.instrumentWithoutRestart(instrumentationName, + instrumentationArgs, + instrumentationWatcher, + instrumentationUiConnection, + targetInfo); + } + @Override + public void updateUiTranslationState(android.os.IBinder activityToken, + int state, + android.view.translation.TranslationSpec sourceSpec, + android.view.translation.TranslationSpec targetSpec, + java.util.List<android.view.autofill.AutofillId> viewIds, + android.view.translation.UiTranslationSpec uiTranslationSpec) + throws android.os.RemoteException { + mBase.updateUiTranslationState(activityToken, + state, + sourceSpec, + targetSpec, + viewIds, + uiTranslationSpec); + } + @Override + public void scheduleTimeoutService(android.os.IBinder token, + int startId) + throws android.os.RemoteException { + mBase.scheduleTimeoutService(token, + startId); + } + @Override + public void scheduleTimeoutServiceForType(android.os.IBinder token, + int startId, + int fgsType) + throws android.os.RemoteException { + mBase.scheduleTimeoutServiceForType(token, + startId, + fgsType); + } + @Override + public void schedulePing(android.os.RemoteCallback pong) + throws android.os.RemoteException { + mBase.schedulePing(pong); + } +} diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 6433f2c1c25d..1c4ffbb812a4 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -2656,6 +2656,7 @@ public final class CachedAppOptimizer { // PIDs that run out of async binder buffer when being frozen ArraySet<Integer> pidsAsync = (mFreezerBinderAsyncThreshold < 0) ? null : new ArraySet<>(); + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "binderErrorSync"); for (int i = 0; i < pids.size(); i++) { int current = pids.get(i); try { @@ -2684,6 +2685,7 @@ public final class CachedAppOptimizer { Slog.w(TAG_AM, "Unable to query binder frozen stats for pid " + current); } } + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); // TODO: when kernel binder driver supports, poll the binder status directly. // Binderfs stats, like other debugfs files, is not a reliable interface. But it's the @@ -2693,6 +2695,8 @@ public final class CachedAppOptimizer { if (pidsAsync == null || pidsAsync.size() == 0) { return; } + + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "binderErrorAsync"); new BinderfsStatsReader().handleFreeAsyncSpace( // Check if the frozen process has pending async calls pidsAsync::contains, @@ -2710,5 +2714,6 @@ public final class CachedAppOptimizer { // Log the error if binderfs stats can't be accesses or correctly parsed exception -> Slog.e(TAG_AM, "Unable to parse binderfs stats")); + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } } diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index a74c4896dd8c..3e71d003f455 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -151,7 +151,7 @@ class ProcessRecord implements WindowProcessListener { * (in which case we are in the process of launching the app). */ @CompositeRWLock({"mService", "mProcLock"}) - private IApplicationThread mThread; + private ApplicationThreadDeferred mThread; /** * Instance of {@link #mThread} that will always meet the {@code oneway} @@ -737,15 +737,15 @@ class ProcessRecord implements WindowProcessListener { } @GuardedBy({"mService", "mProcLock"}) - public void makeActive(IApplicationThread thread, ProcessStatsService tracker) { + public void makeActive(ApplicationThreadDeferred thread, ProcessStatsService tracker) { mProfile.onProcessActive(thread, tracker); mThread = thread; if (mPid == Process.myPid()) { - mOnewayThread = new SameProcessApplicationThread(thread, FgThread.getHandler()); + mOnewayThread = new SameProcessApplicationThread(mThread, FgThread.getHandler()); } else { - mOnewayThread = thread; + mOnewayThread = mThread; } - mWindowProcessController.setThread(thread); + mWindowProcessController.setThread(mThread); if (mWindowProcessController.useFifoUiScheduling()) { mService.mSpecifiedFifoProcesses.add(this); } @@ -1436,14 +1436,17 @@ class ProcessRecord implements WindowProcessListener { void onProcessFrozen() { mProfile.onProcessFrozen(); + if (mThread != null) mThread.onProcessPaused(); } void onProcessUnfrozen() { + if (mThread != null) mThread.onProcessUnpaused(); mProfile.onProcessUnfrozen(); mServices.onProcessUnfrozen(); } void onProcessFrozenCancelled() { + if (mThread != null) mThread.onProcessPausedCancelled(); mServices.onProcessFrozenCancelled(); } diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 932b9c0324fa..7f43fae72d60 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -210,6 +210,7 @@ public class SettingsToPropertiesMapper { "safety_center", "sensors", "spoon", + "stability", "statsd", "system_performance", "system_sw_touch", diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 9b380ff12e2d..d21478807d4d 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -176,3 +176,11 @@ flag { bug: "330682397" is_fixed_read_only: true } + +flag { + name: "defer_binders_when_paused" + namespace: "system_performance" + is_fixed_read_only: true + description: "Defer submitting binder calls to paused processes." + bug: "327038797" +} diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java new file mode 100644 index 000000000000..317c91e9a289 --- /dev/null +++ b/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java @@ -0,0 +1,54 @@ +/* + * 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.crashrecovery; + +import android.content.Context; + +import com.android.server.PackageWatchdog; +import com.android.server.RescueParty; +import com.android.server.SystemService; + + +/** This class encapsulate the lifecycle methods of CrashRecovery module. */ +public class CrashRecoveryModule { + private static final String TAG = "CrashRecoveryModule"; + + /** Lifecycle definition for CrashRecovery module. */ + public static class Lifecycle extends SystemService { + private Context mSystemContext; + private PackageWatchdog mPackageWatchdog; + + public Lifecycle(Context context) { + super(context); + mSystemContext = context; + mPackageWatchdog = PackageWatchdog.getInstance(context); + } + + @Override + public void onStart() { + RescueParty.registerHealthObserver(mSystemContext); + mPackageWatchdog.noteBoot(); + } + + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + mPackageWatchdog.onPackagesReady(); + } + } + } +} diff --git a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING index 4a66bac2e4ec..615db345635c 100644 --- a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING +++ b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING @@ -7,6 +7,9 @@ "include-filter": "com.android.server.RescuePartyTest" } ] + }, + { + "name": "CrashRecoveryModuleTests" } ] }
\ No newline at end of file diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java index 9b37418bb6fe..515e70495f9e 100644 --- a/services/core/java/com/android/server/display/BrightnessRangeController.java +++ b/services/core/java/com/android/server/display/BrightnessRangeController.java @@ -22,6 +22,7 @@ import android.os.IBinder; import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.brightness.clamper.HdrClamper; +import com.android.server.display.config.HighBrightnessModeData; import com.android.server.display.feature.DisplayManagerFlags; import java.io.PrintWriter; @@ -157,7 +158,7 @@ class BrightnessRangeController { private void updateHdrClamper(DisplayDeviceInfo info, IBinder token, DisplayDeviceConfig displayDeviceConfig) { if (mUseHdrClamper) { - DisplayDeviceConfig.HighBrightnessModeData hbmData = + HighBrightnessModeData hbmData = displayDeviceConfig.getHighBrightnessModeData(); float minimumHdrPercentOfScreen = hbmData == null ? -1f : hbmData.minimumHdrPercentOfScreen; diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index e4db634c0e26..f5231ae0abe6 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -53,9 +53,9 @@ import com.android.server.display.config.DisplayBrightnessPoint; import com.android.server.display.config.DisplayConfiguration; import com.android.server.display.config.DisplayQuirks; import com.android.server.display.config.EvenDimmerBrightnessData; -import com.android.server.display.config.HbmTiming; import com.android.server.display.config.HdrBrightnessData; import com.android.server.display.config.HighBrightnessMode; +import com.android.server.display.config.HighBrightnessModeData; import com.android.server.display.config.HysteresisLevels; import com.android.server.display.config.IdleScreenRefreshRateTimeout; import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint; @@ -75,8 +75,6 @@ import com.android.server.display.config.RefreshRateRange; import com.android.server.display.config.RefreshRateThrottlingMap; import com.android.server.display.config.RefreshRateThrottlingPoint; import com.android.server.display.config.RefreshRateZone; -import com.android.server.display.config.SdrHdrRatioMap; -import com.android.server.display.config.SdrHdrRatioPoint; import com.android.server.display.config.SensorData; import com.android.server.display.config.ThermalStatus; import com.android.server.display.config.ThermalThrottling; @@ -302,6 +300,19 @@ import javax.xml.datatype.DatatypeConfigurationException; * <brightnessIncreaseDurationMillis>10000</brightnessIncreaseDurationMillis> * <brightnessDecreaseDebounceMillis>13000</brightnessDecreaseDebounceMillis> * <brightnessDecreaseDurationMillis>10000</brightnessDecreaseDurationMillis> + * <minimumHdrPercentOfScreenForNbm>0.2</minimumHdrPercentOfScreenForNbm> + * <minimumHdrPercentOfScreenForHbm>0.5</minimumHdrPercentOfScreenForHbm> + * <allowInLowPowerMode>true</allowInLowPowerMode> + * <sdrHdrRatioMap> + * <point> + * <first>2.0</first> + * <second>4.0</second> + * </point> + * <point> + * <first>100</first> + * <second>8.0</second> + * </point> + * </sdrHdrRatioMap> * </hdrBrightnessConfig> * <luxThrottling> * <brightnessLimitMap> @@ -659,9 +670,6 @@ public class DisplayDeviceConfig { // Invalid value of AutoBrightness brightening and darkening light debounce private static final int INVALID_AUTO_BRIGHTNESS_LIGHT_DEBOUNCE = -1; - @VisibleForTesting - static final float HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT = 0.5f; - private final Context mContext; // The details of the ambient light sensor associated with this display. @@ -743,13 +751,13 @@ public class DisplayDeviceConfig { private Spline mNitsToBacklightSpline; private List<String> mQuirks; - private boolean mIsHighBrightnessModeEnabled = false; + @Nullable private HighBrightnessModeData mHbmData; @Nullable private PowerThrottlingConfigData mPowerThrottlingConfigData; private DensityMapping mDensityMapping; private String mLoadedFrom = null; - private Spline mSdrToHdrRatioSpline; + // Represents the auto-brightness brightening light debounce. private long mAutoBrightnessBrighteningLightDebounce = @@ -872,7 +880,7 @@ public class DisplayDeviceConfig { private final DisplayManagerFlags mFlags; @VisibleForTesting - DisplayDeviceConfig(Context context, DisplayManagerFlags flags) { + public DisplayDeviceConfig(Context context, DisplayManagerFlags flags) { mContext = context; mFlags = flags; } @@ -1155,7 +1163,7 @@ public class DisplayDeviceConfig { * @return true if there is sdrHdrRatioMap, false otherwise. */ public boolean hasSdrToHdrRatioSpline() { - return mSdrToHdrRatioSpline != null; + return mHbmData != null && mHbmData.sdrToHdrRatioSpline != null; } /** @@ -1165,7 +1173,8 @@ public class DisplayDeviceConfig { * @return the HDR brightness or BRIGHTNESS_INVALID when no mapping exists. */ public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio) { - if (mSdrToHdrRatioSpline == null) { + Spline sdrToHdrSpline = mHbmData != null ? mHbmData.sdrToHdrRatioSpline : null; + if (sdrToHdrSpline == null) { return PowerManager.BRIGHTNESS_INVALID; } @@ -1175,7 +1184,7 @@ public class DisplayDeviceConfig { return PowerManager.BRIGHTNESS_INVALID; } - float ratio = Math.min(mSdrToHdrRatioSpline.interpolate(nits), maxDesiredHdrSdrRatio); + float ratio = Math.min(sdrToHdrSpline.interpolate(nits), maxDesiredHdrSdrRatio); float hdrNits = nits * ratio; if (getNitsToBacklightSpline() == null) { return PowerManager.BRIGHTNESS_INVALID; @@ -1321,13 +1330,11 @@ public class DisplayDeviceConfig { * @return high brightness mode configuration data for the display. */ public HighBrightnessModeData getHighBrightnessModeData() { - if (!mIsHighBrightnessModeEnabled || mHbmData == null) { + if (mHbmData == null || !mHbmData.isHighBrightnessModeEnabled) { return null; } - HighBrightnessModeData hbmData = new HighBrightnessModeData(); - mHbmData.copyTo(hbmData); - return hbmData; + return mHbmData; } /** @@ -1604,11 +1611,10 @@ public class DisplayDeviceConfig { + ", mBacklightMaximum=" + mBacklightMaximum + ", mBrightnessDefault=" + mBrightnessDefault + ", mQuirks=" + mQuirks - + ", mIsHighBrightnessModeEnabled=" + mIsHighBrightnessModeEnabled + "\n" + "mLuxThrottlingData=" + mLuxThrottlingData + ", mHbmData=" + mHbmData - + ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline + + ", mThermalBrightnessThrottlingDataMapByThrottlingId=" + mThermalBrightnessThrottlingDataMapByThrottlingId + "\n" @@ -1715,7 +1721,7 @@ public class DisplayDeviceConfig { } @VisibleForTesting - boolean initFromFile(File configFile) { + public boolean initFromFile(File configFile) { if (!configFile.exists()) { // Display configuration files aren't required to exist. return false; @@ -1740,7 +1746,23 @@ public class DisplayDeviceConfig { loadBrightnessMap(config); loadThermalThrottlingConfig(config); loadPowerThrottlingConfigData(config); - loadHighBrightnessModeData(config); + // Backlight and evenDimmer data should be loaded for HbmData + mHbmData = HighBrightnessModeData.loadHighBrightnessModeData(config, (hbm) -> { + float transitionPointBacklightScale = hbm.getTransitionPoint_all().floatValue(); + if (transitionPointBacklightScale >= mBacklightMaximum) { + throw new IllegalArgumentException("HBM transition point invalid. " + + mHbmData.transitionPoint + " is not less than " + + mBacklightMaximum); + } + return getBrightnessFromBacklight(transitionPointBacklightScale); + }); + if (mHbmData.isHighBrightnessModeEnabled && mHbmData.refreshRateLimit != null) { + // TODO(b/331650248): cleanup, DMD can use mHbmData.refreshRateLimit + mRefreshRateLimitations.add(new RefreshRateLimitation( + DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE, + mHbmData.refreshRateLimit)); + } + loadLuxThrottling(config); loadQuirks(config); loadBrightnessRamps(config); @@ -1938,40 +1960,6 @@ public class DisplayDeviceConfig { constrainNitsAndBacklightArrays(); } - private Spline loadSdrHdrRatioMap(HighBrightnessMode hbmConfig) { - final SdrHdrRatioMap sdrHdrRatioMap = hbmConfig.getSdrHdrRatioMap_all(); - - if (sdrHdrRatioMap == null) { - return null; - } - - final List<SdrHdrRatioPoint> points = sdrHdrRatioMap.getPoint(); - final int size = points.size(); - if (size == 0) { - return null; - } - - float[] nits = new float[size]; - float[] ratios = new float[size]; - - int i = 0; - for (SdrHdrRatioPoint point : points) { - nits[i] = point.getSdrNits().floatValue(); - if (i > 0) { - if (nits[i] < nits[i - 1]) { - Slog.e(TAG, "sdrHdrRatioMap must be non-decreasing, ignoring rest " - + " of configuration. nits: " + nits[i] + " < " - + nits[i - 1]); - return null; - } - } - ratios[i] = point.getHdrRatio().floatValue(); - ++i; - } - - return Spline.createSpline(nits, ratios); - } - private void loadThermalThrottlingConfig(DisplayConfiguration config) { final ThermalThrottling throttlingConfig = config.getThermalThrottling(); if (throttlingConfig == null) { @@ -2525,49 +2513,6 @@ public class DisplayDeviceConfig { } } - private void loadHighBrightnessModeData(DisplayConfiguration config) { - final HighBrightnessMode hbm = config.getHighBrightnessMode(); - if (hbm != null) { - mIsHighBrightnessModeEnabled = hbm.getEnabled(); - mHbmData = new HighBrightnessModeData(); - mHbmData.minimumLux = hbm.getMinimumLux_all().floatValue(); - float transitionPointBacklightScale = hbm.getTransitionPoint_all().floatValue(); - if (transitionPointBacklightScale >= mBacklightMaximum) { - throw new IllegalArgumentException("HBM transition point invalid. " - + mHbmData.transitionPoint + " is not less than " - + mBacklightMaximum); - } - mHbmData.transitionPoint = - getBrightnessFromBacklight(transitionPointBacklightScale); - final HbmTiming hbmTiming = hbm.getTiming_all(); - mHbmData.timeWindowMillis = hbmTiming.getTimeWindowSecs_all().longValue() * 1000; - mHbmData.timeMaxMillis = hbmTiming.getTimeMaxSecs_all().longValue() * 1000; - mHbmData.timeMinMillis = hbmTiming.getTimeMinSecs_all().longValue() * 1000; - mHbmData.allowInLowPowerMode = hbm.getAllowInLowPowerMode_all(); - final RefreshRateRange rr = hbm.getRefreshRate_all(); - if (rr != null) { - final float min = rr.getMinimum().floatValue(); - final float max = rr.getMaximum().floatValue(); - mRefreshRateLimitations.add(new RefreshRateLimitation( - DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE, min, max)); - } - BigDecimal minHdrPctOfScreen = hbm.getMinimumHdrPercentOfScreen_all(); - if (minHdrPctOfScreen != null) { - mHbmData.minimumHdrPercentOfScreen = minHdrPctOfScreen.floatValue(); - if (mHbmData.minimumHdrPercentOfScreen > 1 - || mHbmData.minimumHdrPercentOfScreen < 0) { - Slog.w(TAG, "Invalid minimum HDR percent of screen: " - + String.valueOf(mHbmData.minimumHdrPercentOfScreen)); - mHbmData.minimumHdrPercentOfScreen = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT; - } - } else { - mHbmData.minimumHdrPercentOfScreen = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT; - } - - mSdrToHdrRatioSpline = loadSdrHdrRatioMap(hbm); - } - } - private void loadLuxThrottling(DisplayConfiguration config) { LuxThrottling cfg = config.getLuxThrottling(); if (cfg != null) { @@ -2921,73 +2866,6 @@ public class DisplayDeviceConfig { } /** - * Container for high brightness mode configuration data. - */ - static class HighBrightnessModeData { - /** Minimum lux needed to enter high brightness mode */ - public float minimumLux; - - /** Brightness level at which we transition from normal to high-brightness. */ - public float transitionPoint; - - /** Whether HBM is allowed when {@code Settings.Global.LOW_POWER_MODE} is active. */ - public boolean allowInLowPowerMode; - - /** Time window for HBM. */ - public long timeWindowMillis; - - /** Maximum time HBM is allowed to be during in a {@code timeWindowMillis}. */ - public long timeMaxMillis; - - /** Minimum time that HBM can be on before being enabled. */ - public long timeMinMillis; - - /** Minimum HDR video size to enter high brightness mode */ - public float minimumHdrPercentOfScreen; - - HighBrightnessModeData() {} - - HighBrightnessModeData(float minimumLux, float transitionPoint, long timeWindowMillis, - long timeMaxMillis, long timeMinMillis, boolean allowInLowPowerMode, - float minimumHdrPercentOfScreen) { - this.minimumLux = minimumLux; - this.transitionPoint = transitionPoint; - this.timeWindowMillis = timeWindowMillis; - this.timeMaxMillis = timeMaxMillis; - this.timeMinMillis = timeMinMillis; - this.allowInLowPowerMode = allowInLowPowerMode; - this.minimumHdrPercentOfScreen = minimumHdrPercentOfScreen; - } - - /** - * Copies the HBM data to the specified parameter instance. - * @param other the instance to copy data to. - */ - public void copyTo(@NonNull HighBrightnessModeData other) { - other.minimumLux = minimumLux; - other.timeWindowMillis = timeWindowMillis; - other.timeMaxMillis = timeMaxMillis; - other.timeMinMillis = timeMinMillis; - other.transitionPoint = transitionPoint; - other.allowInLowPowerMode = allowInLowPowerMode; - other.minimumHdrPercentOfScreen = minimumHdrPercentOfScreen; - } - - @Override - public String toString() { - return "HBM{" - + "minLux: " + minimumLux - + ", transition: " + transitionPoint - + ", timeWindow: " + timeWindowMillis + "ms" - + ", timeMax: " + timeMaxMillis + "ms" - + ", timeMin: " + timeMinMillis + "ms" - + ", allowInLowPowerMode: " + allowInLowPowerMode - + ", minimumHdrPercentOfScreen: " + minimumHdrPercentOfScreen - + "} "; - } - } - - /** * Container for Power throttling configuration data. * TODO(b/302814899): extract to separate class. */ diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 3493381b3064..2f3584cf7cef 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -5240,7 +5240,7 @@ public final class DisplayManagerService extends SystemService { mHandler.sendMessage(msg); mLogicalDisplayMapper - .setDeviceStateLocked(deviceState.getIdentifier()); + .setDeviceStateLocked(deviceState); } } }; diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 76a561b65c57..58309c2c751a 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -87,6 +87,7 @@ import com.android.server.display.brightness.strategy.AutomaticBrightnessStrateg import com.android.server.display.brightness.strategy.DisplayBrightnessStrategyConstants; import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal; import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener; +import com.android.server.display.config.HighBrightnessModeData; import com.android.server.display.config.HysteresisLevels; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.layout.Layout; @@ -2017,7 +2018,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call final DisplayDeviceConfig ddConfig = mDisplayDevice.getDisplayDeviceConfig(); final IBinder displayToken = mDisplayDevice.getDisplayTokenLocked(); final String displayUniqueId = mDisplayDevice.getUniqueId(); - final DisplayDeviceConfig.HighBrightnessModeData hbmData = + final HighBrightnessModeData hbmData = ddConfig != null ? ddConfig.getHighBrightnessModeData() : null; final DisplayDeviceInfo info = mDisplayDevice.getDisplayDeviceInfoLocked(); return mInjector.getHighBrightnessModeController(mHandler, info.width, info.height, @@ -3251,7 +3252,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken, String displayUniqueId, float brightnessMin, - float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData, + float brightnessMax, HighBrightnessModeData hbmData, HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg, Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata, Context context) { diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java index 47176fe331bf..da9eef2d7459 100644 --- a/services/core/java/com/android/server/display/HighBrightnessModeController.java +++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java @@ -36,8 +36,8 @@ import android.view.SurfaceControlHdrLayerInfoListener; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.util.FrameworkStatsLog; -import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData; import com.android.server.display.DisplayManagerService.Clock; +import com.android.server.display.config.HighBrightnessModeData; import com.android.server.display.utils.DebugUtils; import java.io.PrintWriter; diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 4791cd1403e2..d3b41b848583 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -16,12 +16,17 @@ package com.android.server.display; +import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP; +import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE; +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static android.view.Display.DEFAULT_DISPLAY; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; -import android.hardware.devicestate.DeviceStateManager; +import android.hardware.devicestate.DeviceState; +import android.hardware.devicestate.feature.flags.FeatureFlags; +import android.hardware.devicestate.feature.flags.FeatureFlagsImpl; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -198,14 +203,14 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { private final DisplayIdProducer mIdProducer = (isDefault) -> isDefault ? DEFAULT_DISPLAY : sNextNonDefaultDisplayId++; private Layout mCurrentLayout = null; - private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; - private int mPendingDeviceState = DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; - private int mDeviceStateToBeAppliedAfterBoot = - DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; + private DeviceState mDeviceState = INVALID_DEVICE_STATE; + private DeviceState mPendingDeviceState = INVALID_DEVICE_STATE; + private DeviceState mDeviceStateToBeAppliedAfterBoot = INVALID_DEVICE_STATE; private boolean mBootCompleted = false; private boolean mInteractive; private final DisplayManagerFlags mFlags; private final SyntheticModeManager mSyntheticModeManager; + private final FeatureFlags mDeviceStateManagerFlags; LogicalDisplayMapper(@NonNull Context context, FoldSettingProvider foldSettingProvider, FoldGracePeriodProvider foldGracePeriodProvider, @@ -245,6 +250,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { mDeviceStateToLayoutMap = deviceStateToLayoutMap; mFlags = flags; mSyntheticModeManager = syntheticModeManager; + mDeviceStateManagerFlags = new FeatureFlagsImpl(); } @Override @@ -403,8 +409,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // Retrieve the display info for the display that matches the display id. final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(display.getAddress()); if (device == null) { - Slog.w(TAG, "The display device (" + display.getAddress() + "), is not available" - + " for the display state " + mDeviceState); + Slog.w(TAG, "The display device (" + display.getAddress() + + "), is not available for the display state " + mDeviceState.getIdentifier()); return null; } LogicalDisplay logicalDisplay = getDisplayLocked(device, /* includeDisabled= */ true); @@ -431,9 +437,11 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { ipw.println("mBootCompleted=" + mBootCompleted); ipw.println(); - ipw.println("mDeviceState=" + mDeviceState); - ipw.println("mPendingDeviceState=" + mPendingDeviceState); - ipw.println("mDeviceStateToBeAppliedAfterBoot=" + mDeviceStateToBeAppliedAfterBoot); + + ipw.println("mDeviceState=" + mDeviceState.getIdentifier()); + ipw.println("mPendingDeviceState=" + mPendingDeviceState.getIdentifier()); + ipw.println("mDeviceStateToBeAppliedAfterBoot=" + + mDeviceStateToBeAppliedAfterBoot.getIdentifier()); final int logicalDisplayCount = mLogicalDisplays.size(); ipw.println(); @@ -463,7 +471,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { mVirtualDeviceDisplayMapping.put(displayDevice.getUniqueId(), virtualDeviceUniqueId); } - void setDeviceStateLocked(int state) { + void setDeviceStateLocked(DeviceState state) { if (!mBootCompleted) { // The boot animation might still be in progress, we do not want to switch states now // as the boot animation would end up with an incorrect size. @@ -475,15 +483,17 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { return; } - Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState - + ", interactive=" + mInteractive + ", mBootCompleted=" + mBootCompleted); + Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + + mDeviceState.getIdentifier() + ", interactive=" + mInteractive + + ", mBootCompleted=" + mBootCompleted); // As part of a state transition, we may need to turn off some displays temporarily so that // the transition is smooth. Plus, on some devices, only one internal displays can be // on at a time. We use LogicalDisplay.setIsInTransition to mark a display that needs to be // temporarily turned off. - resetLayoutLocked(mDeviceState, state, /* transitionValue= */ true); + resetLayoutLocked(mDeviceState.getIdentifier(), + state.getIdentifier(), /* transitionValue= */ true); mPendingDeviceState = state; - mDeviceStateToBeAppliedAfterBoot = DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; + mDeviceStateToBeAppliedAfterBoot = INVALID_DEVICE_STATE; final boolean wakeDevice = shouldDeviceBeWoken(mPendingDeviceState, mDeviceState, mInteractive, mBootCompleted); final boolean sleepDevice = shouldDeviceBePutToSleep(mPendingDeviceState, mDeviceState, @@ -498,7 +508,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { } if (DEBUG) { - Slog.d(TAG, "Postponing transition to state: " + mPendingDeviceState); + Slog.d(TAG, "Postponing transition to state: " + mPendingDeviceState.getIdentifier()); } // Send the transitioning phase updates to DisplayManager so that the displays can // start turning OFF in preparation for the new layout. @@ -533,8 +543,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { void onBootCompleted() { synchronized (mSyncRoot) { mBootCompleted = true; - if (mDeviceStateToBeAppliedAfterBoot - != DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER) { + if (!mDeviceStateToBeAppliedAfterBoot.equals(INVALID_DEVICE_STATE)) { setDeviceStateLocked(mDeviceStateToBeAppliedAfterBoot); } } @@ -563,11 +572,18 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { * @see #setDeviceStateLocked */ @VisibleForTesting - boolean shouldDeviceBeWoken(int pendingState, int currentState, boolean isInteractive, - boolean isBootCompleted) { - return mDeviceStatesOnWhichToWakeUp.get(pendingState) - && !mDeviceStatesOnWhichToWakeUp.get(currentState) - && !isInteractive && isBootCompleted; + boolean shouldDeviceBeWoken(DeviceState pendingState, DeviceState currentState, + boolean isInteractive, boolean isBootCompleted) { + if (mDeviceStateManagerFlags.deviceStatePropertyMigration()) { + return pendingState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE) + && !currentState.equals(INVALID_DEVICE_STATE) + && !currentState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE) + && !isInteractive && isBootCompleted; + } else { + return mDeviceStatesOnWhichToWakeUp.get(pendingState.getIdentifier()) + && !mDeviceStatesOnWhichToWakeUp.get(currentState.getIdentifier()) + && !isInteractive && isBootCompleted; + } } /** @@ -588,14 +604,22 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { * @see #setDeviceStateLocked */ @VisibleForTesting - boolean shouldDeviceBePutToSleep(int pendingState, int currentState, boolean isInteractive, - boolean isBootCompleted) { - return currentState != DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER - && mDeviceStatesOnWhichToSelectiveSleep.get(pendingState) - && !mDeviceStatesOnWhichToSelectiveSleep.get(currentState) - && isInteractive - && isBootCompleted - && !mFoldSettingProvider.shouldStayAwakeOnFold(); + boolean shouldDeviceBePutToSleep(DeviceState pendingState, DeviceState currentState, + boolean isInteractive, boolean isBootCompleted) { + if (mDeviceStateManagerFlags.deviceStatePropertyMigration()) { + return pendingState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP) + && !currentState.equals(INVALID_DEVICE_STATE) + && !currentState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP) + && isInteractive + && isBootCompleted + && !mFoldSettingProvider.shouldStayAwakeOnFold(); + } else { + return mDeviceStatesOnWhichToSelectiveSleep.get(pendingState.getIdentifier()) + && !mDeviceStatesOnWhichToSelectiveSleep.get(currentState.getIdentifier()) + && isInteractive + && isBootCompleted + && !mFoldSettingProvider.shouldStayAwakeOnFold(); + } } private boolean areAllTransitioningDisplaysOffLocked() { @@ -618,27 +642,25 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { } private void transitionToPendingStateLocked() { - resetLayoutLocked(mDeviceState, mPendingDeviceState, /* transitionValue= */ false); + resetLayoutLocked(mDeviceState.getIdentifier(), + mPendingDeviceState.getIdentifier(), /* transitionValue= */ false); mDeviceState = mPendingDeviceState; - mPendingDeviceState = DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; + mPendingDeviceState = INVALID_DEVICE_STATE; applyLayoutLocked(); updateLogicalDisplaysLocked(); } private void finishStateTransitionLocked(boolean force) { - if (mPendingDeviceState == DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER) { + if (mPendingDeviceState.equals(INVALID_DEVICE_STATE)) { return; } - final boolean waitingToWakeDevice = mDeviceStatesOnWhichToWakeUp.get(mPendingDeviceState) - && !mDeviceStatesOnWhichToWakeUp.get(mDeviceState) - && !mInteractive && mBootCompleted; + final boolean waitingToWakeDevice = shouldDeviceBeWoken(mPendingDeviceState, mDeviceState, + mInteractive, mBootCompleted); // The device should only wait for sleep if #shouldStayAwakeOnFold method returns false. // If not, device should be marked ready for transition immediately. - final boolean waitingToSleepDevice = mDeviceStatesOnWhichToSelectiveSleep.get( - mPendingDeviceState) - && !mDeviceStatesOnWhichToSelectiveSleep.get(mDeviceState) - && mInteractive && mBootCompleted && !shouldStayAwakeOnFold(); + final boolean waitingToSleepDevice = shouldDeviceBePutToSleep(mPendingDeviceState, + mDeviceState, mInteractive, mBootCompleted) && !shouldStayAwakeOnFold(); final boolean displaysOff = areAllTransitioningDisplaysOffLocked(); final boolean isReadyToTransition = displaysOff && !waitingToWakeDevice @@ -1104,7 +1126,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { */ private void applyLayoutLocked() { final Layout oldLayout = mCurrentLayout; - mCurrentLayout = mDeviceStateToLayoutMap.get(mDeviceState); + mCurrentLayout = mDeviceStateToLayoutMap.get(mDeviceState.getIdentifier()); Slog.i(TAG, "Applying layout: " + mCurrentLayout + ", Previous layout: " + oldLayout); // Go through each of the displays in the current layout set. @@ -1120,7 +1142,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(address); if (device == null) { Slog.w(TAG, "applyLayoutLocked: The display device (" + address + "), is not " - + "available for the display state " + mDeviceState); + + "available for the display state " + mDeviceState.getIdentifier()); continue; } diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java index 902daa4b7ce2..5c2db35717c5 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java +++ b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java @@ -133,7 +133,7 @@ public class HdrClamper { // new token not null and hdr min % of the screen is set, subscribe. // e.g. for virtual display, HBM data will be missing and HdrListener // should not be registered - if (displayToken != null && mHdrListener.mHdrMinPixels >= 0) { + if (displayToken != null && mHdrListener.mHdrMinPixels >= 0 && hasBrightnessLimits()) { mHdrListener.register(displayToken); mRegisteredDisplayToken = displayToken; } @@ -179,6 +179,10 @@ public class HdrClamper { pw.println(" mAutoBrightnessEnabled=" + mAutoBrightnessEnabled); } + private boolean hasBrightnessLimits() { + return mHdrBrightnessData != null && !mHdrBrightnessData.maxBrightnessLimits.isEmpty(); + } + private void reset() { if (mMaxBrightness == PowerManager.BRIGHTNESS_MAX && mDesiredMaxBrightness == PowerManager.BRIGHTNESS_MAX && mTransitionRate == -1f @@ -214,11 +218,11 @@ public class HdrClamper { mDesiredMaxBrightness = expectedMaxBrightness; long debounceTime; if (mDesiredMaxBrightness > mMaxBrightness) { - debounceTime = mHdrBrightnessData.mBrightnessIncreaseDebounceMillis; - mDesiredTransitionRate = mHdrBrightnessData.mScreenBrightnessRampIncrease; + debounceTime = mHdrBrightnessData.brightnessIncreaseDebounceMillis; + mDesiredTransitionRate = mHdrBrightnessData.screenBrightnessRampIncrease; } else { - debounceTime = mHdrBrightnessData.mBrightnessDecreaseDebounceMillis; - mDesiredTransitionRate = mHdrBrightnessData.mScreenBrightnessRampDecrease; + debounceTime = mHdrBrightnessData.brightnessDecreaseDebounceMillis; + mDesiredTransitionRate = mHdrBrightnessData.screenBrightnessRampDecrease; } mHandler.removeCallbacks(mDebouncer); @@ -232,7 +236,7 @@ public class HdrClamper { float foundAmbientBoundary = Float.MAX_VALUE; float foundMaxBrightness = PowerManager.BRIGHTNESS_MAX; for (Map.Entry<Float, Float> brightnessPoint : - data.mMaxBrightnessLimits.entrySet()) { + data.maxBrightnessLimits.entrySet()) { float ambientBoundary = brightnessPoint.getKey(); // find ambient lux upper boundary closest to current ambient lux if (ambientBoundary > ambientLux && ambientBoundary < foundAmbientBoundary) { diff --git a/services/core/java/com/android/server/display/config/DisplayDeviceConfigUtils.java b/services/core/java/com/android/server/display/config/DisplayDeviceConfigUtils.java new file mode 100644 index 000000000000..5b4e8d51a168 --- /dev/null +++ b/services/core/java/com/android/server/display/config/DisplayDeviceConfigUtils.java @@ -0,0 +1,63 @@ +/* + * 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.display.config; + +import android.annotation.Nullable; +import android.util.Slog; +import android.util.Spline; + +import java.math.BigDecimal; +import java.util.List; +import java.util.function.Function; + +public class DisplayDeviceConfigUtils { + private static final String TAG = "DisplayDeviceConfigUtils"; + + /** + * Create Spline from generic data + * @param points - points for Spline in format (x0, y0), (x1, y1) etc + * @param xExtractor - extract X component from generic data + * @param yExtractor - extract Y component from generic data + */ + @Nullable + public static <T> Spline createSpline(List<T> points, Function<T, BigDecimal> xExtractor, + Function<T, BigDecimal> yExtractor) { + int size = points.size(); + if (size == 0) { + return null; + } + + float[] x = new float[size]; + float[] y = new float[size]; + + int i = 0; + for (T point : points) { + x[i] = xExtractor.apply(point).floatValue(); + if (i > 0) { + if (x[i] <= x[i - 1]) { + Slog.e(TAG, "spline control points must be strictly increasing, ignoring " + + "configuration. x: " + x[i] + " <= " + x[i - 1]); + return null; + } + } + y[i] = yExtractor.apply(point).floatValue(); + ++i; + } + + return Spline.createSpline(x, y); + } +} diff --git a/services/core/java/com/android/server/display/config/HdrBrightnessData.java b/services/core/java/com/android/server/display/config/HdrBrightnessData.java index 837fbf7ca17c..c9408077e5af 100644 --- a/services/core/java/com/android/server/display/config/HdrBrightnessData.java +++ b/services/core/java/com/android/server/display/config/HdrBrightnessData.java @@ -16,63 +16,138 @@ package com.android.server.display.config; +import static com.android.server.display.config.HighBrightnessModeData.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT; + import android.annotation.Nullable; +import android.util.Spline; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.DisplayBrightnessState; +import java.math.BigDecimal; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Brightness config for HDR content + * <pre> + * {@code + * <displayConfiguration> + * ... + * <hdrBrightnessConfig> + * <brightnessMap> + * <point> + * <first>500</first> + * <second>0.3</second> + * </point> + * <point> + * <first>1200</first> + * <second>0.6</second> + * </point> + * </brightnessMap> + * <brightnessIncreaseDebounceMillis>1000</brightnessIncreaseDebounceMillis> + * <brightnessIncreaseDurationMillis>10000</brightnessIncreaseDurationMillis> + * <brightnessDecreaseDebounceMillis>13000</brightnessDecreaseDebounceMillis> + * <brightnessDecreaseDurationMillis>10000</brightnessDecreaseDurationMillis> + * <minimumHdrPercentOfScreenForNbm>0.2</minimumHdrPercentOfScreenForNbm> + * <minimumHdrPercentOfScreenForHbm>0.5</minimumHdrPercentOfScreenForHbm> + * <allowInLowPowerMode>true</allowInLowPowerMode> + * <sdrHdrRatioMap> + * <point> + * <first>2.0</first> + * <second>4.0</second> + * </point> + * <point> + * <first>100</first> + * <second>8.0</second> + * </point> + * </sdrHdrRatioMap> + * </hdrBrightnessConfig> + * ... + * </displayConfiguration> + * } + * </pre> */ public class HdrBrightnessData { + private static final String TAG = "HdrBrightnessData"; /** * Lux to brightness map */ - public final Map<Float, Float> mMaxBrightnessLimits; + public final Map<Float, Float> maxBrightnessLimits; /** * Debounce time for brightness increase */ - public final long mBrightnessIncreaseDebounceMillis; + public final long brightnessIncreaseDebounceMillis; /** * Brightness increase animation speed */ - public final float mScreenBrightnessRampIncrease; + public final float screenBrightnessRampIncrease; /** * Debounce time for brightness decrease */ - public final long mBrightnessDecreaseDebounceMillis; + public final long brightnessDecreaseDebounceMillis; /** * Brightness decrease animation speed */ - public final float mScreenBrightnessRampDecrease; + public final float screenBrightnessRampDecrease; + + /** + * Min Hdr layer size to start hdr brightness boost up to high brightness mode transition point + */ + public final float minimumHdrPercentOfScreenForNbm; + + /** + * Min Hdr layer size to start hdr brightness boost above high brightness mode transition point + */ + public final float minimumHdrPercentOfScreenForHbm; + + /** + * If Hdr brightness boost allowed in low power mode + */ + public final boolean allowInLowPowerMode; + + /** + * brightness to boost ratio spline + */ + @Nullable + public final Spline sdrToHdrRatioSpline; @VisibleForTesting public HdrBrightnessData(Map<Float, Float> maxBrightnessLimits, long brightnessIncreaseDebounceMillis, float screenBrightnessRampIncrease, - long brightnessDecreaseDebounceMillis, float screenBrightnessRampDecrease) { - mMaxBrightnessLimits = maxBrightnessLimits; - mBrightnessIncreaseDebounceMillis = brightnessIncreaseDebounceMillis; - mScreenBrightnessRampIncrease = screenBrightnessRampIncrease; - mBrightnessDecreaseDebounceMillis = brightnessDecreaseDebounceMillis; - mScreenBrightnessRampDecrease = screenBrightnessRampDecrease; + long brightnessDecreaseDebounceMillis, float screenBrightnessRampDecrease, + float minimumHdrPercentOfScreenForNbm, float minimumHdrPercentOfScreenForHbm, + boolean allowInLowPowerMode, @Nullable Spline sdrToHdrRatioSpline) { + this.maxBrightnessLimits = maxBrightnessLimits; + this.brightnessIncreaseDebounceMillis = brightnessIncreaseDebounceMillis; + this.screenBrightnessRampIncrease = screenBrightnessRampIncrease; + this.brightnessDecreaseDebounceMillis = brightnessDecreaseDebounceMillis; + this.screenBrightnessRampDecrease = screenBrightnessRampDecrease; + this.minimumHdrPercentOfScreenForNbm = minimumHdrPercentOfScreenForNbm; + this.minimumHdrPercentOfScreenForHbm = minimumHdrPercentOfScreenForHbm; + this.allowInLowPowerMode = allowInLowPowerMode; + this.sdrToHdrRatioSpline = sdrToHdrRatioSpline; } @Override public String toString() { return "HdrBrightnessData {" - + "mMaxBrightnessLimits: " + mMaxBrightnessLimits - + ", mBrightnessIncreaseDebounceMillis: " + mBrightnessIncreaseDebounceMillis - + ", mScreenBrightnessRampIncrease: " + mScreenBrightnessRampIncrease - + ", mBrightnessDecreaseDebounceMillis: " + mBrightnessDecreaseDebounceMillis - + ", mScreenBrightnessRampDecrease: " + mScreenBrightnessRampDecrease + + "mMaxBrightnessLimits: " + maxBrightnessLimits + + ", mBrightnessIncreaseDebounceMillis: " + brightnessIncreaseDebounceMillis + + ", mScreenBrightnessRampIncrease: " + screenBrightnessRampIncrease + + ", mBrightnessDecreaseDebounceMillis: " + brightnessDecreaseDebounceMillis + + ", mScreenBrightnessRampDecrease: " + screenBrightnessRampDecrease + + ", minimumHdrPercentOfScreenForNbm: " + minimumHdrPercentOfScreenForNbm + + ", minimumHdrPercentOfScreenForHbm: " + minimumHdrPercentOfScreenForHbm + + ", allowInLowPowerMode: " + allowInLowPowerMode + + ", sdrToHdrRatioSpline: " + sdrToHdrRatioSpline + "} "; } @@ -83,7 +158,7 @@ public class HdrBrightnessData { public static HdrBrightnessData loadConfig(DisplayConfiguration config) { HdrBrightnessConfig hdrConfig = config.getHdrBrightnessConfig(); if (hdrConfig == null) { - return null; + return getFallbackData(config.getHighBrightnessMode()); } List<NonNegativeFloatToFloatPoint> points = hdrConfig.getBrightnessMap().getPoint(); @@ -92,10 +167,59 @@ public class HdrBrightnessData { brightnessLimits.put(point.getFirst().floatValue(), point.getSecond().floatValue()); } + float minHdrPercentForHbm = hdrConfig.getMinimumHdrPercentOfScreenForHbm() != null + ? hdrConfig.getMinimumHdrPercentOfScreenForHbm().floatValue() + : getFallbackHdrPercent(config.getHighBrightnessMode()); + + float minHdrPercentForNbm = hdrConfig.getMinimumHdrPercentOfScreenForNbm() != null + ? hdrConfig.getMinimumHdrPercentOfScreenForNbm().floatValue() : minHdrPercentForHbm; + return new HdrBrightnessData(brightnessLimits, hdrConfig.getBrightnessIncreaseDebounceMillis().longValue(), hdrConfig.getScreenBrightnessRampIncrease().floatValue(), hdrConfig.getBrightnessDecreaseDebounceMillis().longValue(), - hdrConfig.getScreenBrightnessRampDecrease().floatValue()); + hdrConfig.getScreenBrightnessRampDecrease().floatValue(), + minHdrPercentForNbm, minHdrPercentForHbm, hdrConfig.getAllowInLowPowerMode(), + getSdrHdrRatioSpline(hdrConfig, config.getHighBrightnessMode())); + } + + @Nullable + private static HdrBrightnessData getFallbackData(HighBrightnessMode hbm) { + if (hbm == null) { + return null; + } + float fallbackPercent = getFallbackHdrPercent(hbm); + Spline fallbackSpline = getFallbackSdrHdrRatioSpline(hbm); + return new HdrBrightnessData(Collections.emptyMap(), + 0, DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET, + 0, DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET, + fallbackPercent, fallbackPercent, false, fallbackSpline); + } + + private static float getFallbackHdrPercent(HighBrightnessMode hbm) { + BigDecimal minHdrPctOfScreen = hbm != null ? hbm.getMinimumHdrPercentOfScreen_all() : null; + return minHdrPctOfScreen != null ? minHdrPctOfScreen.floatValue() + : HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT; + } + + @Nullable + private static Spline getSdrHdrRatioSpline(HdrBrightnessConfig hdrConfig, + HighBrightnessMode hbm) { + NonNegativeFloatToFloatMap sdrHdrRatioMap = hdrConfig.getSdrHdrRatioMap(); + if (sdrHdrRatioMap == null) { + return getFallbackSdrHdrRatioSpline(hbm); + } + return DisplayDeviceConfigUtils.createSpline(sdrHdrRatioMap.getPoint(), + NonNegativeFloatToFloatPoint::getFirst, NonNegativeFloatToFloatPoint::getSecond); + } + + @Nullable + private static Spline getFallbackSdrHdrRatioSpline(HighBrightnessMode hbm) { + SdrHdrRatioMap fallbackMap = hbm != null ? hbm.getSdrHdrRatioMap_all() : null; + if (fallbackMap == null) { + return null; + } + return DisplayDeviceConfigUtils.createSpline(fallbackMap.getPoint(), + SdrHdrRatioPoint::getSdrNits, SdrHdrRatioPoint::getHdrRatio); } } diff --git a/services/core/java/com/android/server/display/config/HighBrightnessModeData.java b/services/core/java/com/android/server/display/config/HighBrightnessModeData.java new file mode 100644 index 000000000000..dc2f976748e6 --- /dev/null +++ b/services/core/java/com/android/server/display/config/HighBrightnessModeData.java @@ -0,0 +1,157 @@ +/* + * 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.display.config; + +import android.annotation.Nullable; +import android.util.Slog; +import android.util.Spline; +import android.view.SurfaceControl; + +import com.android.internal.annotations.VisibleForTesting; + +import java.math.BigDecimal; +import java.util.function.Function; + +/** + * Container for high brightness mode configuration data. + */ +public class HighBrightnessModeData { + private static final String TAG = "HighBrightnessModeData"; + + static final float HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT = 0.5f; + + /** Minimum lux needed to enter high brightness mode */ + public final float minimumLux; + + /** Brightness level at which we transition from normal to high-brightness. */ + public final float transitionPoint; + + /** Whether HBM is allowed when {@code Settings.Global.LOW_POWER_MODE} is active. */ + public final boolean allowInLowPowerMode; + + /** Time window for HBM. */ + public final long timeWindowMillis; + + /** Maximum time HBM is allowed to be during in a {@code timeWindowMillis}. */ + public final long timeMaxMillis; + + /** Minimum time that HBM can be on before being enabled. */ + public final long timeMinMillis; + + /** Minimum HDR video size to enter high brightness mode */ + public final float minimumHdrPercentOfScreen; + + @Nullable + public final Spline sdrToHdrRatioSpline; + + @Nullable + public final SurfaceControl.RefreshRateRange refreshRateLimit; + + public final boolean isHighBrightnessModeEnabled; + + @VisibleForTesting + public HighBrightnessModeData(float minimumLux, float transitionPoint, long timeWindowMillis, + long timeMaxMillis, long timeMinMillis, boolean allowInLowPowerMode, + float minimumHdrPercentOfScreen, @Nullable Spline sdrToHdrRatioSpline, + @Nullable SurfaceControl.RefreshRateRange refreshRateLimit, + boolean isHighBrightnessModeEnabled) { + this.minimumLux = minimumLux; + this.transitionPoint = transitionPoint; + this.timeWindowMillis = timeWindowMillis; + this.timeMaxMillis = timeMaxMillis; + this.timeMinMillis = timeMinMillis; + this.allowInLowPowerMode = allowInLowPowerMode; + this.minimumHdrPercentOfScreen = minimumHdrPercentOfScreen; + this.sdrToHdrRatioSpline = sdrToHdrRatioSpline; + this.refreshRateLimit = refreshRateLimit; + this.isHighBrightnessModeEnabled = isHighBrightnessModeEnabled; + } + + @Override + public String toString() { + return "HBM{" + + "minLux: " + minimumLux + + ", transition: " + transitionPoint + + ", timeWindow: " + timeWindowMillis + "ms" + + ", timeMax: " + timeMaxMillis + "ms" + + ", timeMin: " + timeMinMillis + "ms" + + ", allowInLowPowerMode: " + allowInLowPowerMode + + ", minimumHdrPercentOfScreen: " + minimumHdrPercentOfScreen + + ", mSdrToHdrRatioSpline=" + sdrToHdrRatioSpline + + ", refreshRateLimit=" + refreshRateLimit + + ", isHighBrightnessModeEnabled=" + isHighBrightnessModeEnabled + + "} "; + } + + /** + * Loads HighBrightnessModeData from DisplayConfiguration + */ + public static HighBrightnessModeData loadHighBrightnessModeData(DisplayConfiguration config, + Function<HighBrightnessMode, Float> transitionPointProvider) { + final HighBrightnessMode hbm = config.getHighBrightnessMode(); + float minimumLux = 0f; + float transitionPoint = 0f; + long timeWindowMillis = 0L; + long timeMaxMillis = 0L; + long timeMinMillis = 0L; + boolean allowInLowPowerMode = false; + float minimumHdrPercentOfScreen = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT; + Spline sdrToHdrRatioSpline = null; + SurfaceControl.RefreshRateRange refreshRateLimit = null; + boolean isEnabled = false; + + if (hbm != null) { + minimumLux = hbm.getMinimumLux_all().floatValue(); + transitionPoint = transitionPointProvider.apply(hbm); + HbmTiming hbmTiming = hbm.getTiming_all(); + timeWindowMillis = hbmTiming.getTimeWindowSecs_all().longValue() * 1000; + timeMaxMillis = hbmTiming.getTimeMaxSecs_all().longValue() * 1000; + timeMinMillis = hbmTiming.getTimeMinSecs_all().longValue() * 1000; + allowInLowPowerMode = hbm.getAllowInLowPowerMode_all(); + BigDecimal minHdrPctOfScreen = hbm.getMinimumHdrPercentOfScreen_all(); + if (minHdrPctOfScreen != null) { + minimumHdrPercentOfScreen = minHdrPctOfScreen.floatValue(); + if (minimumHdrPercentOfScreen > 1 || minimumHdrPercentOfScreen < 0) { + Slog.w(TAG, "Invalid minimum HDR percent of screen: " + + minimumHdrPercentOfScreen); + minimumHdrPercentOfScreen = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT; + } + } + + sdrToHdrRatioSpline = loadSdrHdrRatioMap(hbm); + RefreshRateRange rr = hbm.getRefreshRate_all(); + if (rr != null) { + refreshRateLimit = new SurfaceControl.RefreshRateRange( + rr.getMinimum().floatValue(), rr.getMaximum().floatValue()); + } + isEnabled = hbm.getEnabled(); + } + return new HighBrightnessModeData(minimumLux, transitionPoint, + timeWindowMillis, timeMaxMillis, timeMinMillis, allowInLowPowerMode, + minimumHdrPercentOfScreen, sdrToHdrRatioSpline, refreshRateLimit, isEnabled); + + } + + private static Spline loadSdrHdrRatioMap(HighBrightnessMode hbmConfig) { + final SdrHdrRatioMap sdrHdrRatioMap = hbmConfig.getSdrHdrRatioMap_all(); + if (sdrHdrRatioMap == null) { + return null; + } + return DisplayDeviceConfigUtils.createSpline(sdrHdrRatioMap.getPoint(), + SdrHdrRatioPoint::getSdrNits, SdrHdrRatioPoint::getHdrRatio); + } +} diff --git a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java index 88da6fb94754..550f68fbc94c 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java @@ -660,7 +660,11 @@ public class HdmiCecNetwork { .setPortId(physicalAddressToPortId(physicalAddress)) .setDeviceType(type) .build(); - updateCecDevice(updatedDeviceInfo); + if (deviceInfo.getPhysicalAddress() != physicalAddress) { + addCecDevice(updatedDeviceInfo); + } else { + updateCecDevice(updatedDeviceInfo); + } } } diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java index 82ecb4acb197..91ab8720ac0b 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java @@ -225,17 +225,17 @@ final class AdditionalSubtypeMapRepository { sWriter.startThread(); } - static void initialize(@NonNull Handler handler, @NonNull Context context) { + static void initialize(@NonNull Handler ioHandler, @NonNull Context context) { final UserManagerInternal userManagerInternal = LocalServices.getService(UserManagerInternal.class); - handler.post(() -> { + ioHandler.post(() -> { userManagerInternal.addUserLifecycleListener( new UserManagerInternal.UserLifecycleListener() { @Override public void onUserCreated(UserInfo user, @Nullable Object token) { final int userId = user.id; sWriter.onUserCreated(userId); - handler.post(() -> { + ioHandler.post(() -> { synchronized (ImfLock.class) { if (!sPerUserMap.contains(userId)) { final AdditionalSubtypeMap additionalSubtypeMap = @@ -257,7 +257,7 @@ final class AdditionalSubtypeMapRepository { public void onUserRemoved(UserInfo user) { final int userId = user.id; sWriter.onUserRemoved(userId); - handler.post(() -> { + ioHandler.post(() -> { synchronized (ImfLock.class) { sPerUserMap.remove(userId); } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 875380f76949..7daf9582cdd6 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -26,6 +26,7 @@ import static android.provider.Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE; import static android.provider.Settings.Secure.STYLUS_HANDWRITING_ENABLED; import static android.server.inputmethod.InputMethodManagerServiceProto.BACK_DISPOSITION; import static android.server.inputmethod.InputMethodManagerServiceProto.BOUND_TO_METHOD; +import static android.server.inputmethod.InputMethodManagerServiceProto.CONCURRENT_MULTI_USER_MODE_ENABLED; import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_ATTRIBUTE; import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_CLIENT; import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_FOCUSED_WINDOW_SOFT_INPUT_MODE; @@ -78,6 +79,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; import android.content.res.Resources; import android.hardware.input.InputManager; import android.inputmethodservice.InputMethodService; @@ -345,8 +347,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private int mCurrentUserId; /** Holds all user related data */ - @GuardedBy("ImfLock.class") - private UserDataRepository mUserDataRepository; + @SharedByAllUsersField + private final UserDataRepository mUserDataRepository; final WindowManagerInternal mWindowManagerInternal; private final ActivityManagerInternal mActivityManagerInternal; @@ -922,11 +924,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. * {@link SystemService} used to publish and manage the lifecycle of * {@link InputMethodManagerService}. */ - public static final class Lifecycle extends SystemService { + public static final class Lifecycle extends SystemService + implements UserManagerInternal.UserLifecycleListener { private final InputMethodManagerService mService; public Lifecycle(Context context) { this(context, createServiceForProduction(context)); + + // For production code, hook up user lifecycle + mService.mUserManagerInternal.addUserLifecycleListener(this); } @VisibleForTesting @@ -958,6 +964,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */); ioThread.start(); + SecureSettingsWrapper.setContentResolver(context.getContentResolver()); + return new InputMethodManagerService(context, shouldEnableConcurrentMultiUserMode(context), thread.getLooper(), Handler.createAsync(ioThread.getLooper()), @@ -1005,6 +1013,19 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @Override + public void onUserCreated(UserInfo user, @Nullable Object token) { + // Called directly from UserManagerService. Do not block the calling thread. + } + + @Override + public void onUserRemoved(UserInfo user) { + // Called directly from UserManagerService. Do not block the calling thread. + final int userId = user.id; + SecureSettingsWrapper.onUserRemoved(userId); + mService.mUserDataRepository.remove(userId); + } + + @Override public void onUserUnlocking(@NonNull TargetUser user) { // Called on ActivityManager thread. SecureSettingsWrapper.onUserUnlocking(user.getUserIdentifier()); @@ -1017,13 +1038,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Called on ActivityManager thread. final int userId = user.getUserIdentifier(); SecureSettingsWrapper.onUserStarting(userId); - synchronized (ImfLock.class) { - if (mService.mConcurrentMultiUserModeEnabled) { - if (mService.mCurrentUserId != userId && mService.mSystemReady) { - mService.initializeVisibleBackgroundUserLocked(userId); + mService.mIoHandler.post(() -> { + synchronized (ImfLock.class) { + if (mService.mConcurrentMultiUserModeEnabled) { + if (mService.mCurrentUserId != userId && mService.mSystemReady) { + mService.initializeVisibleBackgroundUserLocked(userId); + } } } - } + }); } } @@ -1081,7 +1104,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mConcurrentMultiUserModeEnabled = concurrentMultiUserModeEnabled; mContext = context; mRes = context.getResources(); - SecureSettingsWrapper.onStart(mContext); mHandler = Handler.createAsync(uiLooper, this); mIoHandler = ioHandler; @@ -1106,14 +1128,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Search for InputMethodSettingsRepository.put() to find where and when it's actually // being updated. In general IMMS should refrain from exposing the existence of IMEs // until systemReady(). - InputMethodSettingsRepository.initialize(mHandler, mContext); - AdditionalSubtypeMapRepository.initialize(mHandler, mContext); + InputMethodSettingsRepository.initialize(mIoHandler, mContext); + AdditionalSubtypeMapRepository.initialize(mIoHandler, mContext); mCurrentUserId = mActivityManagerInternal.getCurrentUserId(); @SuppressWarnings("GuardedBy") final IntFunction<InputMethodBindingController> bindingControllerFactory = userId -> new InputMethodBindingController(userId, InputMethodManagerService.this); - mUserDataRepository = new UserDataRepository(mHandler, mUserManagerInternal, + mUserDataRepository = new UserDataRepository( bindingControllerForTesting != null ? bindingControllerForTesting : bindingControllerFactory); @@ -4560,6 +4582,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. proto.write(BACK_DISPOSITION, bindingController.getBackDisposition()); proto.write(IME_WINDOW_VISIBILITY, bindingController.getImeWindowVis()); proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard()); + proto.write(CONCURRENT_MULTI_USER_MODE_ENABLED, mConcurrentMultiUserModeEnabled); proto.end(token); } } @@ -6013,6 +6036,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final var userData = getUserData(userId); p.println("Current Input Method Manager state:"); + p.println(" concurrentMultiUserModeEnabled" + mConcurrentMultiUserModeEnabled); final List<InputMethodInfo> methodList = settings.getMethodList(); int numImes = methodList.size(); p.println(" Input Methods:"); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java index 68924b5f370f..a4d8ee566755 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java @@ -54,16 +54,16 @@ final class InputMethodSettingsRepository { sPerUserMap.put(userId, obj); } - static void initialize(@NonNull Handler handler, @NonNull Context context) { + static void initialize(@NonNull Handler ioHandler, @NonNull Context context) { final UserManagerInternal userManagerInternal = LocalServices.getService(UserManagerInternal.class); - handler.post(() -> { + ioHandler.post(() -> { userManagerInternal.addUserLifecycleListener( new UserManagerInternal.UserLifecycleListener() { @Override public void onUserRemoved(UserInfo user) { final int userId = user.id; - handler.post(() -> { + ioHandler.post(() -> { synchronized (ImfLock.class) { sPerUserMap.remove(userId); } diff --git a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java index 4764e4fadd6e..e7cff20ea2cb 100644 --- a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java +++ b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java @@ -20,10 +20,7 @@ import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.app.ActivityManagerInternal; import android.content.ContentResolver; -import android.content.Context; -import android.content.pm.UserInfo; import android.provider.Settings; import android.util.ArrayMap; import android.util.ArraySet; @@ -321,30 +318,13 @@ final class SecureSettingsWrapper { } /** - * Called when {@link InputMethodManagerService} is starting. + * Called when the system is starting. * - * @param context the {@link Context} to be used. + * @param contentResolver the {@link ContentResolver} to be used */ @AnyThread - static void onStart(@NonNull Context context) { - sContentResolver = context.getContentResolver(); - - final int userId = LocalServices.getService(ActivityManagerInternal.class) - .getCurrentUserId(); - final UserManagerInternal userManagerInternal = - LocalServices.getService(UserManagerInternal.class); - putOrGet(userId, createImpl(userManagerInternal, userId)); - - userManagerInternal.addUserLifecycleListener( - new UserManagerInternal.UserLifecycleListener() { - @Override - public void onUserRemoved(UserInfo user) { - synchronized (sUserMap) { - sUserMap.remove(userId); - } - } - } - ); + static void setContentResolver(@NonNull ContentResolver contentResolver) { + sContentResolver = contentResolver; } /** @@ -377,6 +357,18 @@ final class SecureSettingsWrapper { } /** + * Called when a user is being removed. + * + * @param userId the ID of the user whose storage is being removed. + */ + @AnyThread + static void onUserRemoved(@UserIdInt int userId) { + synchronized (sUserMap) { + sUserMap.remove(userId); + } + } + + /** * Put the given string {@code value} to {@code key}. * * @param key a secure settings key. diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java index 5cd980f2150b..98d7548d3dd2 100644 --- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java +++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java @@ -16,11 +16,10 @@ package com.android.server.inputmethod; +import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.content.pm.UserInfo; -import android.os.Handler; import android.util.SparseArray; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ImeTracker; @@ -29,52 +28,63 @@ import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; -import com.android.server.pm.UserManagerInternal; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Consumer; import java.util.function.IntFunction; final class UserDataRepository { - @GuardedBy("ImfLock.class") + private final ReentrantReadWriteLock mUserDataLock = new ReentrantReadWriteLock(); + + @GuardedBy("mUserDataLock") private final SparseArray<UserData> mUserData = new SparseArray<>(); private final IntFunction<InputMethodBindingController> mBindingControllerFactory; - @GuardedBy("ImfLock.class") + @AnyThread @NonNull UserData getOrCreate(@UserIdInt int userId) { - UserData userData = mUserData.get(userId); - if (userData == null) { - userData = new UserData(userId, mBindingControllerFactory.apply(userId)); - mUserData.put(userId, userData); + mUserDataLock.writeLock().lock(); + try { + UserData userData = mUserData.get(userId); + if (userData == null) { + userData = new UserData(userId, mBindingControllerFactory.apply(userId)); + mUserData.put(userId, userData); + } + return userData; + } finally { + mUserDataLock.writeLock().unlock(); } - return userData; } - @GuardedBy("ImfLock.class") + @AnyThread void forAllUserData(Consumer<UserData> consumer) { - for (int i = 0; i < mUserData.size(); i++) { - consumer.accept(mUserData.valueAt(i)); + final SparseArray<UserData> copiedArray; + mUserDataLock.readLock().lock(); + try { + copiedArray = mUserData.clone(); + } finally { + mUserDataLock.readLock().unlock(); + } + for (int i = 0; i < copiedArray.size(); i++) { + consumer.accept(copiedArray.valueAt(i)); } } UserDataRepository( - @NonNull Handler handler, @NonNull UserManagerInternal userManagerInternal, @NonNull IntFunction<InputMethodBindingController> bindingControllerFactory) { mBindingControllerFactory = bindingControllerFactory; - userManagerInternal.addUserLifecycleListener( - new UserManagerInternal.UserLifecycleListener() { - @Override - public void onUserRemoved(UserInfo user) { - final int userId = user.id; - handler.post(() -> { - synchronized (ImfLock.class) { - mUserData.remove(userId); - } - }); - } - }); + } + + @AnyThread + void remove(@UserIdInt int userId) { + mUserDataLock.writeLock().lock(); + try { + mUserData.remove(userId); + } finally { + mUserDataLock.writeLock().unlock(); + } } /** Placeholder for all IMMS user specific fields */ diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index e3d5c54f1d5e..803b125cbabc 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -17,6 +17,7 @@ package com.android.server.media.projection; import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION; +import static android.Manifest.permission.RECORD_SENSITIVE_CONTENT; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; @@ -34,10 +35,12 @@ import android.Manifest; import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.app.ActivityManagerInternal; import android.app.ActivityOptions.LaunchCookie; import android.app.AppOpsManager; import android.app.IProcessObserver; +import android.app.KeyguardManager; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; @@ -78,6 +81,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.server.LocalServices; +import com.android.server.SystemConfig; import com.android.server.SystemService; import com.android.server.Watchdog; import com.android.server.wm.WindowManagerInternal; @@ -132,6 +136,7 @@ public final class MediaProjectionManagerService extends SystemService private final ActivityManagerInternal mActivityManagerInternal; private final PackageManager mPackageManager; private final WindowManagerInternal mWmInternal; + private final KeyguardManager mKeyguardManager; private final MediaRouter mMediaRouter; private final MediaRouterCallback mMediaRouterCallback; @@ -147,7 +152,9 @@ public final class MediaProjectionManagerService extends SystemService this(context, new Injector()); } - @VisibleForTesting MediaProjectionManagerService(Context context, Injector injector) { + @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE) + @VisibleForTesting + MediaProjectionManagerService(Context context, Injector injector) { super(context); mContext = context; mInjector = injector; @@ -163,9 +170,47 @@ public final class MediaProjectionManagerService extends SystemService mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE); mMediaRouterCallback = new MediaRouterCallback(); mMediaProjectionMetricsLogger = injector.mediaProjectionMetricsLogger(context); + mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); + mKeyguardManager.addKeyguardLockedStateListener( + mContext.getMainExecutor(), this::onKeyguardLockedStateChanged); Watchdog.getInstance().addMonitor(this); } + /** + * In order to record the keyguard, the MediaProjection package must be either: + * - a holder of RECORD_SENSITIVE_CONTENT permission, or + * - be one of the bugreport whitelisted packages + */ + private boolean canCaptureKeyguard() { + if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) { + return true; + } + synchronized (mLock) { + if (mProjectionGrant == null || mProjectionGrant.packageName == null) { + return false; + } + if (mPackageManager.checkPermission(RECORD_SENSITIVE_CONTENT, + mProjectionGrant.packageName) + == PackageManager.PERMISSION_GRANTED) { + return true; + } + return SystemConfig.getInstance().getBugreportWhitelistedPackages() + .contains(mProjectionGrant.packageName); + } + } + + @VisibleForTesting + void onKeyguardLockedStateChanged(boolean isKeyguardLocked) { + if (!isKeyguardLocked) return; + synchronized (mLock) { + if (mProjectionGrant != null && !canCaptureKeyguard()) { + Slog.d(TAG, "Content Recording: Stopped MediaProjection" + + " due to keyguard lock"); + mProjectionGrant.stop(); + } + } + } + /** Functional interface for providing time. */ @VisibleForTesting interface Clock { @@ -1252,6 +1297,11 @@ public final class MediaProjectionManagerService extends SystemService @Override public void notifyVirtualDisplayCreated(int displayId) { notifyVirtualDisplayCreated_enforcePermission(); + if (mKeyguardManager.isKeyguardLocked() && !canCaptureKeyguard()) { + Slog.w(TAG, "Content Recording: Keyguard locked, aborting MediaProjection"); + stop(); + return; + } synchronized (mLock) { mVirtualDisplayId = displayId; diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java index ec5d96df3430..4a82057ed2a2 100644 --- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java +++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java @@ -106,11 +106,11 @@ class ZenModeEventLogger { /** * Potentially log a zen mode change if the provided config and policy changes warrant it. * - * @param prevInfo ZenModeInfo (zen mode setting, config, policy) prior to this change - * @param newInfo ZenModeInfo after this change takes effect - * @param callingUid the calling UID associated with the change; may be used to attribute the - * change to a particular package or determine if this is a user action - * @param origin The origin of the Zen change. + * @param prevInfo ZenModeInfo (zen mode setting, config, policy) prior to this change + * @param newInfo ZenModeInfo after this change takes effect + * @param callingUid the calling UID associated with the change; may be used to attribute the + * change to a particular package or determine if this is a user action + * @param origin The origin of the Zen change. */ public final void maybeLogZenChange(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid, @ConfigChangeOrigin int origin) { @@ -127,6 +127,9 @@ class ZenModeEventLogger { /** * Reassign callingUid in mChangeState if we have more specific information that warrants it * (for instance, if the change is automatic and due to an automatic rule change). + * + * <p>When Flags.modesUi() is enabled, we reassign the calling UID to the package UID in all + * changes whose source is not system or system UI, as long as there is only one rule changed. */ private void maybeReassignCallingUid() { int userId = Process.INVALID_UID; @@ -145,12 +148,23 @@ class ZenModeEventLogger { userId = mChangeState.mNewConfig.user; // mNewConfig must not be null if enabler exists } - // The conditions where we should consider reassigning UID for an automatic rule change: + // The conditions where we should consider reassigning UID for an automatic rule change + // (pre-modes_ui): // - we've determined it's not a user action // - our current best guess is that the calling uid is system/sysui + // When Flags.modesUi() is true, we get the package UID for the changed rule, as long as: + // - the change does not originate from the system based on change origin + // - there is only one rule changed if (mChangeState.getChangedRuleType() == RULE_TYPE_AUTOMATIC) { - if (mChangeState.getIsUserAction() || !mChangeState.isFromSystemOrSystemUi()) { - return; + if (Flags.modesUi()) { + // ignore anything whose origin is system + if (mChangeState.isFromSystemOrSystemUi()) { + return; + } + } else { + if (mChangeState.getIsUserAction() || !mChangeState.isFromSystemOrSystemUi()) { + return; + } } // Only try to get the package UID if there's exactly one changed automatic rule. If @@ -202,7 +216,8 @@ class ZenModeEventLogger { /* int32 package_uid = 7 */ mChangeState.getPackageUid(), /* DNDPolicyProto current_policy = 8 */ mChangeState.getDNDPolicyProto(), /* bool are_channels_bypassing = 9 */ mChangeState.getAreChannelsBypassing(), - /* ActiveRuleType active_rule_types = 10 */ mChangeState.getActiveRuleTypes()); + /* ActiveRuleType active_rule_types = 10 */ mChangeState.getActiveRuleTypes(), + /* ChangeOrigin change_origin = 11 */ mChangeState.getChangeOrigin()); } /** @@ -235,7 +250,8 @@ class ZenModeEventLogger { ZenModeConfig mPrevConfig, mNewConfig; NotificationManager.Policy mPrevPolicy, mNewPolicy; int mCallingUid = Process.INVALID_UID; - @ConfigChangeOrigin int mOrigin = ZenModeConfig.UPDATE_ORIGIN_UNKNOWN; + @ConfigChangeOrigin + int mOrigin = ZenModeConfig.UPDATE_ORIGIN_UNKNOWN; private void init(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid, @ConfigChangeOrigin int origin) { @@ -388,7 +404,8 @@ class ZenModeEventLogger { * rules available. */ @SuppressLint("WrongConstant") // special case for log-only type on manual rule - @NonNull List<ZenRule> activeRulesList(ZenModeConfig config) { + @NonNull + List<ZenRule> activeRulesList(ZenModeConfig config) { ArrayList<ZenRule> rules = new ArrayList<>(); if (config == null) { return rules; @@ -548,6 +565,17 @@ class ZenModeEventLogger { } /** + * Get the config change origin associated with this change, which is stored in mOrigin. + * Only useable if modes_ui is true. + */ + int getChangeOrigin() { + if (Flags.modesUi()) { + return mOrigin; + } + return 0; + } + + /** * Convert the new policy to a DNDPolicyProto format for output in logs. * * <p>If {@code mNewZenMode} is {@code ZEN_MODE_OFF} (which can mean either no rules diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 009e9b862b0f..303371bd9a92 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -801,8 +801,9 @@ final class InstallPackageHelper { try { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityLaunchAllowed(false); - target.sendIntent(context, 0, fillIn, null /* onFinished*/, null /* handler */, - null /* requiredPermission */, options.toBundle()); + target.sendIntent(context, 0, fillIn, + null /* requiredPermission */, options.toBundle(), + null /* executor */, null /* onFinished*/); } catch (IntentSender.SendIntentException ignored) { } } @@ -2628,18 +2629,28 @@ final class InstallPackageHelper { String packageName = pkgLite.packageName; synchronized (mPm.mLock) { - // Package which currently owns the data that the new package will own if installed. - // If an app is uninstalled while keeping data (e.g. adb uninstall -k), installedPkg - // will be null whereas dataOwnerPkg will contain information about the package - // which was uninstalled while keeping its data. - AndroidPackage dataOwnerPkg = mPm.mPackages.get(packageName); PackageSetting dataOwnerPs = mPm.mSettings.getPackageLPr(packageName); - if (dataOwnerPkg == null) { - if (dataOwnerPs != null) { - dataOwnerPkg = dataOwnerPs.getPkg(); + if (dataOwnerPs == null) { + if (requiredInstalledVersionCode != PackageManager.VERSION_CODE_HIGHEST) { + String errorMsg = "Required installed version code was " + + requiredInstalledVersionCode + + " but package is not installed"; + Slog.w(TAG, errorMsg); + return Pair.create( + PackageManager.INSTALL_FAILED_WRONG_INSTALLED_VERSION, errorMsg); } + // The package doesn't exist in the system, don't need to check the version + // replacing. + return Pair.create(PackageManager.INSTALL_SUCCEEDED, null); } + // Package which currently owns the data that the new package will own if installed. + // If an app is uninstalled while keeping data (e.g. adb uninstall -k), dataOwnerPkg + // will be null whereas dataOwnerPs will contain information about the package + // which was uninstalled while keeping its data. The AndroidPackage object that the + // PackageSetting refers to is the same object that is stored in mPackages. + AndroidPackage dataOwnerPkg = dataOwnerPs.getPkg(); + if (requiredInstalledVersionCode != PackageManager.VERSION_CODE_HIGHEST) { if (dataOwnerPkg == null) { String errorMsg = "Required installed version code was " @@ -2661,7 +2672,27 @@ final class InstallPackageHelper { } } - if (dataOwnerPkg != null && !dataOwnerPkg.isSdkLibrary()) { + // If dataOwnerPkg is null but dataOwnerPs is not null, there is always data on + // some users. Wwe should do the downgrade check. E.g. DELETE_KEEP_DATA and + // archived apps + if (dataOwnerPkg == null) { + if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags, + dataOwnerPs.isDebuggable())) { + // The data exists on some users and downgrade is not permitted; a lower + // version of the app will not be allowed. + try { + PackageManagerServiceUtils.checkDowngrade(dataOwnerPs, pkgLite); + } catch (PackageManagerException e) { + String errorMsg = "Downgrade detected on app uninstalled with" + + " DELETE_KEEP_DATA: " + e.getMessage(); + Slog.w(TAG, errorMsg); + return Pair.create( + PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg); + } + } + // dataOwnerPs.getPkg() is not null on system apps case. Don't need to consider + // system apps case like below. + } else if (dataOwnerPkg != null && !dataOwnerPkg.isSdkLibrary()) { if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags, dataOwnerPkg.isDebuggable())) { // Downgrade is not permitted; a lower version of the app will not be allowed diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 0d1095f5656d..6b7b702b9157 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -1194,8 +1194,9 @@ public class PackageArchiver { MODE_BACKGROUND_ACTIVITY_START_DENIED); for (IntentSender intentSender : unarchiveIntentSenders) { try { - intentSender.sendIntent(mContext, 0, broadcastIntent, /* onFinished= */ null, - /* handler= */ null, /* requiredPermission= */ null, options.toBundle()); + intentSender.sendIntent(mContext, 0, broadcastIntent, + /* requiredPermission */ null, options.toBundle(), + /* executor */ null, /* onFinished */ null); } catch (IntentSender.SendIntentException e) { Slog.e(TAG, TextUtils.formatSimple("Failed to send unarchive intent"), e); } finally { @@ -1328,8 +1329,9 @@ public class PackageArchiver { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityStartMode( MODE_BACKGROUND_ACTIVITY_START_DENIED); - statusReceiver.sendIntent(mContext, 0, intent, /* onFinished= */ null, - /* handler= */ null, /* requiredPermission= */ null, options.toBundle()); + statusReceiver.sendIntent(mContext, 0, intent, + /* requiredPermission */ null, options.toBundle(), + /* executor */ null, /* onFinished */ null); } catch (IntentSender.SendIntentException e) { Slog.e( TAG, diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index b93dcdc93a82..f615ca1da2e2 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -1581,8 +1581,9 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements try { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityLaunchAllowed(false); - callback.sendIntent(mContext, 0, intent, null /* onFinished*/, - null /* handler */, null /* requiredPermission */, options.toBundle()); + callback.sendIntent(mContext, 0, intent, + null /* requiredPermission */, options.toBundle(), + null /* executor */, null /* onFinished*/); } catch (SendIntentException ignore) { } }); @@ -1912,8 +1913,9 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements try { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityLaunchAllowed(false); - mTarget.sendIntent(mContext, 0, fillIn, null /* onFinished*/, - null /* handler */, null /* requiredPermission */, options.toBundle()); + mTarget.sendIntent(mContext, 0, fillIn, + null /* requiredPermission */, options.toBundle(), + null /* executor */, null /* onFinished*/); } catch (SendIntentException ignored) { } } @@ -1945,8 +1947,9 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements try { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityLaunchAllowed(false); - mTarget.sendIntent(mContext, 0, fillIn, null /* onFinished*/, - null /* handler */, null /* requiredPermission */, options.toBundle()); + mTarget.sendIntent(mContext, 0, fillIn, + null /* requiredPermission */, options.toBundle(), + null /* executor */, null /* onFinished*/); } catch (SendIntentException ignored) { } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 47a79a3c4051..ff8a69de35bc 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -5053,8 +5053,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { try { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityLaunchAllowed(false); - target.sendIntent(mContext, 0 /* code */, intent, null /* onFinished */, - null /* handler */, null /* requiredPermission */, options.toBundle()); + target.sendIntent(mContext, 0 /* code */, intent, + null /* requiredPermission */, options.toBundle(), + null /* executor */, null /* onFinished*/); } catch (IntentSender.SendIntentException ignored) { } } @@ -5447,8 +5448,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { try { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityLaunchAllowed(false); - target.sendIntent(context, 0, fillIn, null /* onFinished */, - null /* handler */, null /* requiredPermission */, options.toBundle()); + target.sendIntent(context, 0, fillIn, + null /* requiredPermission */, options.toBundle(), + null /* executor */, null /* onFinished*/); } catch (IntentSender.SendIntentException ignored) { } } @@ -5496,8 +5498,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { try { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityLaunchAllowed(false); - target.sendIntent(context, 0, fillIn, null /* onFinished */, - null /* handler */, null /* requiredPermission */, options.toBundle()); + target.sendIntent(context, 0, fillIn, + null /* requiredPermission */, options.toBundle(), + null /* executor */, null /* onFinished*/); } catch (IntentSender.SendIntentException ignored) { } } @@ -5533,8 +5536,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { try { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityLaunchAllowed(false); - target.sendIntent(context, 0, intent, null /* onFinished */, - null /* handler */, null /* requiredPermission */, options.toBundle()); + target.sendIntent(context, 0, intent, + null /* requiredPermission */, options.toBundle(), + null /* executor */, null /* onFinished*/); } catch (IntentSender.SendIntentException ignored) { } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 2e63cdbf1823..b5c33cd68d89 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -5034,8 +5034,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityLaunchAllowed(false); pi.sendIntent(null, success ? 1 : 0, null /* intent */, - null /* onFinished*/, null /* handler */, - null /* requiredPermission */, options.toBundle()); + null /* requiredPermission */, options.toBundle(), + null /* executor */, null /* onFinished*/); } catch (SendIntentException e) { Slog.w(TAG, e); } diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 924b36cef79a..c3cac2032a91 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -1419,10 +1419,23 @@ public class PackageManagerServiceUtils { /** * Check and throw if the given before/after packages would be considered a - * downgrade. + * downgrade with {@link PackageSetting}. */ - public static void checkDowngrade(AndroidPackage before, PackageInfoLite after) - throws PackageManagerException { + public static void checkDowngrade(@NonNull PackageSetting before, + @NonNull PackageInfoLite after) throws PackageManagerException { + if (after.getLongVersionCode() < before.getVersionCode()) { + throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE, + "Update version code " + after.versionCode + " is older than current " + + before.getVersionCode()); + } + } + + /** + * Check and throw if the given before/after packages would be considered a + * downgrade with {@link AndroidPackage}. + */ + public static void checkDowngrade(@NonNull AndroidPackage before, + @NonNull PackageInfoLite after) throws PackageManagerException { if (after.getLongVersionCode() < before.getLongVersionCode()) { throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE, "Update version code " + after.versionCode + " is older than current " diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 7870b1735af4..82df527edcc3 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -97,6 +97,7 @@ public class PackageSetting extends SettingBase implements PackageStateInternal FORCE_QUERYABLE_OVERRIDE, SCANNED_AS_STOPPED_SYSTEM_APP, PENDING_RESTORE, + DEBUGGABLE, }) public @interface Flags { } @@ -105,6 +106,7 @@ public class PackageSetting extends SettingBase implements PackageStateInternal private static final int FORCE_QUERYABLE_OVERRIDE = 1 << 2; private static final int SCANNED_AS_STOPPED_SYSTEM_APP = 1 << 3; private static final int PENDING_RESTORE = 1 << 4; + private static final int DEBUGGABLE = 1 << 5; } private int mBooleans; @@ -562,6 +564,20 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return getBoolean(Booleans.PENDING_RESTORE); } + /** + * @see PackageState#isDebuggable + */ + public PackageSetting setDebuggable(boolean value) { + setBoolean(Booleans.DEBUGGABLE, value); + onChanged(); + return this; + } + + @Override + public boolean isDebuggable() { + return getBoolean(Booleans.DEBUGGABLE); + } + @Override public String toString() { return "PackageSetting{" diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java index 9ab6016f3d57..d8ce38e0cd2c 100644 --- a/services/core/java/com/android/server/pm/ScanPackageUtils.java +++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java @@ -437,6 +437,9 @@ final class ScanPackageUtils { pkgSetting.setIsOrphaned(true); } + // update debuggable to packageSetting + pkgSetting.setDebuggable(parsedPackage.isDebuggable()); + // Take care of first install / last update times. final long scanFileTime = getLastModifiedTime(parsedPackage); final long existingFirstInstallTime = userId == UserHandle.USER_ALL diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 39565526f33e..0d16b009d9a5 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -3255,6 +3255,9 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile if (pkg.isPendingRestore()) { serializer.attributeBoolean(null, "pendingRestore", true); } + if (pkg.isDebuggable()) { + serializer.attributeBoolean(null, "debuggable", true); + } if (pkg.isLoading()) { serializer.attributeBoolean(null, "isLoading", true); } @@ -3269,7 +3272,6 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile serializer.attributeInt(null, "appMetadataSource", pkg.getAppMetadataSource()); - writeUsesSdkLibLPw(serializer, pkg.getUsesSdkLibraries(), pkg.getUsesSdkLibrariesVersionsMajor(), pkg.getUsesSdkLibrariesOptional()); @@ -4059,6 +4061,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile long versionCode = 0; boolean installedForceQueryable = false; boolean isPendingRestore = false; + boolean isDebuggable = false; float loadingProgress = 0; long loadingCompletedTime = 0; UUID domainSetId; @@ -4085,6 +4088,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile updateAvailable = parser.getAttributeBoolean(null, "updateAvailable", false); installedForceQueryable = parser.getAttributeBoolean(null, "forceQueryable", false); isPendingRestore = parser.getAttributeBoolean(null, "pendingRestore", false); + isDebuggable = parser.getAttributeBoolean(null, "debuggable", false); loadingProgress = parser.getAttributeFloat(null, "loadingProgress", 0); loadingCompletedTime = parser.getAttributeLongHex(null, "loadingCompletedTime", 0); @@ -4259,6 +4263,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile .setUpdateAvailable(updateAvailable) .setForceQueryableOverride(installedForceQueryable) .setPendingRestore(isPendingRestore) + .setDebuggable(isDebuggable) .setLoadingProgress(loadingProgress) .setLoadingCompletedTime(loadingCompletedTime) .setAppMetadataFilePath(appMetadataFilePath) diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 4a60e452919f..021f7aa9c215 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -4490,8 +4490,9 @@ public class ShortcutService extends IShortcutService.Stub { ActivityOptions options = ActivityOptions.makeBasic() .setPendingIntentBackgroundActivityStartMode( MODE_BACKGROUND_ACTIVITY_START_DENIED); - intentSender.sendIntent(mContext, /* code= */ 0, extras, - /* onFinished=*/ null, /* handler= */ null, null, options.toBundle()); + intentSender.sendIntent(mContext, 0 /* code */, extras, + null /* requiredPermission */, options.toBundle(), + null /* executor */, null /* onFinished*/); } catch (SendIntentException e) { Slog.w(TAG, "sendIntent failed().", e); } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index b9a9d64fc50e..dde9943ccd7b 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1176,6 +1176,30 @@ public class UserManagerService extends IUserManager.Stub { } } + /** Marks the user as slated for deletion during boot if necessary. **/ + @GuardedBy("mUsersLock") + private void markUserForRemovalIfNecessaryLU(UserInfo ui) { + if (!ui.isEphemeral()) { + // User should be ephemeral to be marked for removal. + return; + } + if (ui.preCreated) { + // Avoid marking pre-created users for removal. + return; + } + if (ui.lastLoggedInTime == 0 && ui.isGuest() && Resources.getSystem().getBoolean( + com.android.internal.R.bool.config_guestUserAutoCreated)) { + // Avoid marking auto-created but not-yet-logged-in guest user for removal. Because a + // new one will be created anyway, and this one doesn't have any personal data in it yet + // due to not being logged in. + return; + } + // Mark the user for removal. + addRemovingUserIdLocked(ui.id); + ui.partial = true; + ui.flags |= UserInfo.FLAG_DISABLED; + } + /* Prunes out any partially created or partially removed users. */ private void cleanupPartialUsers() { ArrayList<UserInfo> partials = new ArrayList<>(); @@ -1901,6 +1925,7 @@ public class UserManagerService extends IUserManager.Stub { Slog.i(LOG_TAG, "Quiet mode is already " + enableQuietMode); return; } + UserManager.invalidateQuietModeEnabledCache(); profile.flags ^= UserInfo.FLAG_QUIET_MODE; profileUserData = getUserDataLU(profile.id); } @@ -4340,13 +4365,7 @@ public class UserManagerService extends IUserManager.Stub { || mNextSerialNumber <= userData.info.id) { mNextSerialNumber = userData.info.id + 1; } - if (userData.info.isEphemeral() && !userData.info.preCreated - && userData.info.id != UserHandle.USER_SYSTEM) { - // Mark ephemeral user as slated for deletion. - addRemovingUserIdLocked(userData.info.id); - userData.info.partial = true; - userData.info.flags |= UserInfo.FLAG_DISABLED; - } + markUserForRemovalIfNecessaryLU(userData.info); } } } else if (name.equals(TAG_GUEST_RESTRICTIONS)) { diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java index e0ee199a343d..58761886ecb9 100644 --- a/services/core/java/com/android/server/pm/pkg/PackageState.java +++ b/services/core/java/com/android/server/pm/pkg/PackageState.java @@ -274,6 +274,14 @@ public interface PackageState { boolean isPendingRestore(); /** + * @see ApplicationInfo#FLAG_DEBUGGABLE + * @see R.styleable#AndroidManifestApplication_debuggable + * @see AndroidPackage#isDebuggable + * @hide + */ + boolean isDebuggable(); + + /** * Retrieves the shared user app ID. Note that the actual shared user data is not available here * and must be queried separately. * diff --git a/services/core/java/com/android/server/power/LowPowerStandbyController.java b/services/core/java/com/android/server/power/LowPowerStandbyController.java index fa94b43b6d0e..421471e2eeec 100644 --- a/services/core/java/com/android/server/power/LowPowerStandbyController.java +++ b/services/core/java/com/android/server/power/LowPowerStandbyController.java @@ -1380,10 +1380,7 @@ public class LowPowerStandbyController { Slog.d(TAG, "notifyStandbyPortsChanged"); } - final Intent intent = new Intent(PowerManager.ACTION_LOW_POWER_STANDBY_PORTS_CHANGED); - intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - mContext.sendBroadcastAsUser(intent, UserHandle.ALL, - Manifest.permission.MANAGE_LOW_POWER_STANDBY); + sendExplicitBroadcast(PowerManager.ACTION_LOW_POWER_STANDBY_PORTS_CHANGED); } /** diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index f85b8cc9c1bb..aa5f5a24f179 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -206,7 +206,6 @@ public class Notifier { mPolicy = policy; mFaceDownDetector = faceDownDetector; mScreenUndimDetector = screenUndimDetector; - mWakefulnessSessionObserver = new WakefulnessSessionObserver(mContext, null); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); mInputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class); @@ -214,6 +213,7 @@ public class Notifier { mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); mTrustManager = mContext.getSystemService(TrustManager.class); mVibrator = mContext.getSystemService(Vibrator.class); + mWakefulnessSessionObserver = new WakefulnessSessionObserver(mContext, null); mHandler = new NotifierHandler(looper); mBackgroundExecutor = backgroundExecutor; @@ -813,6 +813,8 @@ public class Notifier { if (DEBUG) { Slog.d(TAG, "onScreenPolicyUpdate: newPolicy=" + newPolicy); } + mWakefulnessSessionObserver.onScreenPolicyUpdate( + SystemClock.uptimeMillis(), displayGroupId, newPolicy); synchronized (mLock) { Message msg = mHandler.obtainMessage(MSG_SCREEN_POLICY); diff --git a/services/core/java/com/android/server/power/WakefulnessSessionObserver.java b/services/core/java/com/android/server/power/WakefulnessSessionObserver.java index d57cd5df41db..3546565480ad 100644 --- a/services/core/java/com/android/server/power/WakefulnessSessionObserver.java +++ b/services/core/java/com/android/server/power/WakefulnessSessionObserver.java @@ -16,8 +16,11 @@ package com.android.server.power; +import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; +import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM; import static android.os.PowerManager.USER_ACTIVITY_EVENT_OTHER; import static android.os.PowerManagerInternal.isInteractive; +import static android.view.Display.DEFAULT_DISPLAY; import static com.android.server.power.PowerManagerService.DEFAULT_SCREEN_OFF_TIMEOUT; import static com.android.server.power.ScreenTimeoutOverridePolicy.RELEASE_REASON_NON_INTERACTIVE; @@ -34,6 +37,9 @@ import android.app.ActivityManager; import android.app.SynchronousUserSwitchObserver; import android.content.Context; import android.database.ContentObserver; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManagerInternal; +import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest; import android.os.Handler; import android.os.PowerManager; import android.os.PowerManagerInternal; @@ -44,9 +50,13 @@ import android.provider.Settings; import android.util.IndentingPrintWriter; import android.util.SparseArray; import android.view.Display; +import android.view.DisplayAddress; +import android.view.DisplayInfo; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BackgroundThread; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.LocalServices; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -117,9 +127,42 @@ public class WakefulnessSessionObserver { @Retention(RetentionPolicy.SOURCE) private @interface OverrideOutcome {} - private static final int DEFAULT_USER_ACTIVITY = USER_ACTIVITY_EVENT_OTHER; - private static final long TIMEOUT_USER_INITIATED_REVERT_THRESHOLD_MILLIS = 5000L; + private static final int POLICY_REASON_UNKNOWN = FrameworkStatsLog + .SCREEN_DIM_REPORTED__POLICY_REASON__UNKNOWN; + @VisibleForTesting + protected static final int POLICY_REASON_OFF_TIMEOUT = FrameworkStatsLog + .SCREEN_DIM_REPORTED__POLICY_REASON__OFF_TIMEOUT; + @VisibleForTesting + protected static final int POLICY_REASON_OFF_POWER_BUTTON = FrameworkStatsLog + .SCREEN_DIM_REPORTED__POLICY_REASON__OFF_POWER_BUTTON; + @VisibleForTesting + protected static final int POLICY_REASON_BRIGHT_UNDIM = FrameworkStatsLog + .SCREEN_DIM_REPORTED__POLICY_REASON__BRIGHT_UNDIM; + @VisibleForTesting + protected static final int POLICY_REASON_BRIGHT_INITIATED_REVERT = FrameworkStatsLog + .SCREEN_DIM_REPORTED__POLICY_REASON__BRIGHT_INITIATED_REVERT; + + /** + * Policy Reason + * {@link android.os.statsd.power.ScreenDimReported.PolicyReason}. + */ + @IntDef(prefix = {"POLICY_REASON_"}, value = { + POLICY_REASON_UNKNOWN, + POLICY_REASON_OFF_TIMEOUT, + POLICY_REASON_OFF_POWER_BUTTON, + POLICY_REASON_BRIGHT_UNDIM, + POLICY_REASON_BRIGHT_INITIATED_REVERT + }) + @Retention(RetentionPolicy.SOURCE) + private @interface PolicyReason {} + + @VisibleForTesting protected static final int DEFAULT_USER_ACTIVITY = USER_ACTIVITY_EVENT_OTHER; + private static final long USER_INITIATED_REVERT_THRESHOLD_MILLIS = 5000L; private static final long SEND_OVERRIDE_TIMEOUT_LOG_THRESHOLD_MILLIS = 1000L; + @VisibleForTesting + protected static final long SCREEN_POLICY_DIM_POWER_OFF_BRIGHT_THRESHOLD_MILLIS = 500L; + + @VisibleForTesting protected static final Object HANDLER_TOKEN = new Object(); private Context mContext; private int mScreenOffTimeoutMs; @@ -130,17 +173,24 @@ public class WakefulnessSessionObserver { protected WakefulnessSessionFrameworkStatsLogger mWakefulnessSessionFrameworkStatsLogger; private final Clock mClock; private final Object mLock = new Object(); + private final Handler mHandler; - public WakefulnessSessionObserver(Context context, Injector injector) { + private DisplayManagerInternal mDisplayManagerInternal; + private int mPhysicalDisplayPortIdForDefaultDisplay; + + public WakefulnessSessionObserver( + Context context, Injector injector) { if (injector == null) { injector = new Injector(); } mContext = context; + mDisplayManagerInternal = injector.getDisplayManagerInternal(); mWakefulnessSessionFrameworkStatsLogger = injector .getWakefulnessSessionFrameworkStatsLogger(); mClock = injector.getClock(); - updateSettingScreenOffTimeout(context); + mHandler = injector.getHandler(); + updateSettingScreenOffTimeout(mContext); try { final UserSwitchObserver observer = new UserSwitchObserver(); @@ -164,6 +214,31 @@ public class WakefulnessSessionObserver { }, UserHandle.USER_ALL); + mPhysicalDisplayPortIdForDefaultDisplay = getPhysicalDisplayPortId(DEFAULT_DISPLAY); + DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); + if (displayManager != null) { + displayManager.registerDisplayListener( + new DisplayManager.DisplayListener() { + @Override + public void onDisplayChanged(int displayId) { + if (displayId == DEFAULT_DISPLAY) { + mPhysicalDisplayPortIdForDefaultDisplay = getPhysicalDisplayPortId( + DEFAULT_DISPLAY); + } + } + + @Override + public void onDisplayAdded(int i) { + } + + @Override + public void onDisplayRemoved(int i) { + } + }, + mHandler, + DisplayManager.EVENT_FLAG_DISPLAY_CHANGED); + } + mPowerGroups.append( Display.DEFAULT_DISPLAY_GROUP, new WakefulnessSessionPowerGroup(Display.DEFAULT_DISPLAY_GROUP)); @@ -186,6 +261,20 @@ public class WakefulnessSessionObserver { } /** + * Track the screen policy + * + * @param eventTime policy changing time, in uptime millis. + * @param powerGroupId Power Group Id for this screen policy + * @param newPolicy Screen Policy defined in {@link DisplayPowerRequest} + */ + public void onScreenPolicyUpdate(long eventTime, int powerGroupId, int newPolicy) { + if (!mPowerGroups.contains(powerGroupId)) { + mPowerGroups.append(powerGroupId, new WakefulnessSessionPowerGroup(powerGroupId)); + } + mPowerGroups.get(powerGroupId).onScreenPolicyUpdate(eventTime, newPolicy); + } + + /** * Track the system wakefulness * * @param powerGroupId Power Group Id for this wakefulness changes @@ -267,6 +356,14 @@ public class WakefulnessSessionObserver { } } + private int getPhysicalDisplayPortId(int displayId) { + if (mDisplayManagerInternal == null) { + return -1; + } + DisplayInfo display = mDisplayManagerInternal.getDisplayInfo(displayId); + return ((DisplayAddress.Physical) display.address).getPort(); + } + private int getScreenOffTimeout() { synchronized (mLock) { return mScreenOffTimeoutMs; @@ -277,10 +374,9 @@ public class WakefulnessSessionObserver { @VisibleForTesting protected class WakefulnessSessionPowerGroup { private static final long TIMEOUT_OFF_RESET_TIMESTAMP = -1; - private int mPowerGroupId; private int mCurrentWakefulness; - private boolean mIsInteractive = false; + @VisibleForTesting protected boolean mIsInteractive = false; // state on start timestamp: will be used in state off to calculate the duration of state on private long mInteractiveStateOnStartTimestamp; @VisibleForTesting @@ -295,6 +391,17 @@ public class WakefulnessSessionObserver { private int mTimeoutOverrideWakeLockCounter = 0; // The timestamp when Override Timeout is set to false private @ScreenTimeoutOverridePolicy.ReleaseReason int mTimeoutOverrideReleaseReason; + // The timestamp when current screen policy is set + private long mCurrentScreenPolicyTimestamp; + // current screen policy + private int mCurrentScreenPolicy; + // The screen policy before the current one + private int mPrevScreenPolicy; + // The previous screen policy duration + private int mPrevScreenPolicyDurationMs; + // The past dim duration + @VisibleForTesting protected int mPastDimDurationMs; + private long mInteractiveOffTimestamp; // The timestamp when state off by timeout occurs // will set TIMEOUT_OFF_RESET_TIMESTAMP if state on or state off by power button private long mTimeoutOffTimestamp; @@ -307,6 +414,10 @@ public class WakefulnessSessionObserver { mPrevUserActivityEvent = DEFAULT_USER_ACTIVITY; mPrevUserActivityTimestamp = -1; mPowerGroupId = powerGroupId; + mCurrentScreenPolicy = mPrevScreenPolicy = POLICY_BRIGHT; + mCurrentScreenPolicyTimestamp = 0; + mPrevScreenPolicyDurationMs = 0; + mPastDimDurationMs = 0; } public void notifyUserActivity(long eventTime, @PowerManager.UserActivityEvent int event) { @@ -320,6 +431,21 @@ public class WakefulnessSessionObserver { mCurrentUserActivityTimestamp = eventTime; } + public void onScreenPolicyUpdate(long eventTime, int newPolicy) { + if (newPolicy == mCurrentScreenPolicy) { + return; + } + + if (newPolicy == POLICY_BRIGHT) { + checkAndLogDimIfQualified(POLICY_REASON_BRIGHT_UNDIM, eventTime); + } + + mPrevScreenPolicy = mCurrentScreenPolicy; + mCurrentScreenPolicy = newPolicy; + mPrevScreenPolicyDurationMs = (int) (eventTime - mCurrentScreenPolicyTimestamp); + mCurrentScreenPolicyTimestamp = eventTime; + } + public void onWakefulnessChangeStarted(int wakefulness, int changeReason, long eventTime) { mCurrentWakefulness = wakefulness; if (mIsInteractive == isInteractive(wakefulness)) { @@ -331,10 +457,10 @@ public class WakefulnessSessionObserver { mInteractiveStateOnStartTimestamp = eventTime; // Log the outcome of screen timeout override (USER INITIATED REVERT), - // when user initiates to revert the screen off state in a short period. + // when user initiates to revert the off state in a short period. if (mTimeoutOffTimestamp != TIMEOUT_OFF_RESET_TIMESTAMP) { - long offToOnDurationMs = eventTime - mTimeoutOffTimestamp; - if (offToOnDurationMs < TIMEOUT_USER_INITIATED_REVERT_THRESHOLD_MILLIS) { + long timeoutOffToOnDurationMs = eventTime - mTimeoutOffTimestamp; + if (timeoutOffToOnDurationMs < USER_INITIATED_REVERT_THRESHOLD_MILLIS) { mWakefulnessSessionFrameworkStatsLogger.logTimeoutOverrideEvent( mPowerGroupId, OVERRIDE_OUTCOME_TIMEOUT_USER_INITIATED_REVERT, @@ -344,11 +470,15 @@ public class WakefulnessSessionObserver { } mTimeoutOffTimestamp = TIMEOUT_OFF_RESET_TIMESTAMP; } + + checkAndLogDimIfQualified(POLICY_REASON_BRIGHT_INITIATED_REVERT, eventTime); + } else { int lastUserActivity = mCurrentUserActivityEvent; long lastUserActivityDurationMs = eventTime - mCurrentUserActivityTimestamp; @OffReason int interactiveStateOffReason = OFF_REASON_UNKNOWN; int reducedInteractiveStateOnDurationMs = 0; + mInteractiveOffTimestamp = eventTime; if (changeReason == PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON) { interactiveStateOffReason = OFF_REASON_POWER_BUTTON; @@ -369,6 +499,9 @@ public class WakefulnessSessionObserver { mSendOverrideTimeoutLogTimestamp = eventTime; mTimeoutOverrideReleaseReason = RELEASE_REASON_UNKNOWN; // reset the reason } + + checkAndLogDimIfQualified(POLICY_REASON_OFF_POWER_BUTTON, eventTime); + } else if (changeReason == PowerManager.GO_TO_SLEEP_REASON_TIMEOUT) { // Interactive Off reason is timeout interactiveStateOffReason = OFF_REASON_TIMEOUT; @@ -393,6 +526,8 @@ public class WakefulnessSessionObserver { // state instantly mTimeoutOffTimestamp = eventTime; } + + checkAndLogDimIfQualified(POLICY_REASON_OFF_TIMEOUT, eventTime); } long interactiveStateOnDurationMs = @@ -462,6 +597,106 @@ public class WakefulnessSessionObserver { } } + private void checkAndLogDimIfQualified( + @PolicyReason int reasonToBeChecked, long eventTime) { + // Only log dim event when DEFAULT_DISPLAY + if (mPowerGroupId != DEFAULT_DISPLAY) { + return; + } + + int dimDurationMs = 0; + int lastUserActivity = mCurrentUserActivityEvent; + int lastUserActivityDurationMs = (int) (eventTime - mCurrentUserActivityTimestamp); + switch (reasonToBeChecked) { + case POLICY_REASON_OFF_TIMEOUT: { + // The policy ordering: + // (1) --DIM--OFF/DOZE->| or (2) --DIM->| because OFF/DOZE hasn't been updated. + dimDurationMs = (int) (eventTime - mCurrentScreenPolicyTimestamp); //(1)--DIM->| + if (mPrevScreenPolicy == POLICY_DIM) { // for (2) --DIM--OFF/DOZE->| + dimDurationMs = mPrevScreenPolicyDurationMs; + } + mWakefulnessSessionFrameworkStatsLogger.logDimEvent( + mPhysicalDisplayPortIdForDefaultDisplay, + reasonToBeChecked, + lastUserActivity, + lastUserActivityDurationMs, + dimDurationMs, + mScreenOffTimeoutMs); + mPastDimDurationMs = dimDurationMs; + return; + } + case POLICY_REASON_OFF_POWER_BUTTON: { + // Power Off will be triggered by USER_ACTIVITY_EVENT_BUTTON + // The metric wants to record the previous activity before EVENT_BUTTON + lastUserActivity = mPrevUserActivityEvent; + lastUserActivityDurationMs = (int) (eventTime - mPrevUserActivityTimestamp); + // the policy ordering: + // (1) ---BRIGHT->| or (2) ---DIM->| because OFF/DOZE hasn't been updated + dimDurationMs = 0; // for (1) ---BRIGHT->| which doesn't have dim (no need log) + if (mCurrentScreenPolicy == POLICY_DIM) { // for (2) ---DIM->| + dimDurationMs = (int) (eventTime - mCurrentScreenPolicyTimestamp); + mWakefulnessSessionFrameworkStatsLogger.logDimEvent( + mPhysicalDisplayPortIdForDefaultDisplay, + reasonToBeChecked, + lastUserActivity, + lastUserActivityDurationMs, + dimDurationMs, + mScreenOffTimeoutMs); + mHandler.removeCallbacksAndMessages(HANDLER_TOKEN); + } + + mPastDimDurationMs = dimDurationMs; + return; + } + case POLICY_REASON_BRIGHT_UNDIM: { + // Has checked the latest screen policy is POLICY_BRIGHT in onScreenPolicyUpdate + if (mCurrentScreenPolicy == POLICY_DIM) { // policy ordering: --DIM--BRIGHT->| + int savedDimDurationMs = (int) (eventTime - mCurrentScreenPolicyTimestamp); + int savedLastUserActivity = lastUserActivity; + int savedLastUserActivityDurationMs = lastUserActivityDurationMs; + + // For the undim case --DIM--BRIGHT->|, it needs wait 500 ms to + // differentiate between "power button off" case, which is + // --DIM--BRIGHT(<500ms)--OFF/DOZE->| + // [Method] Wait 500 ms to see whether triggers power button off or not. + // [Reason] We got --DIM--BRIGHT->|. However, if BRIGHT is so short (<500ms) + // and follows OFF/DOZE, it represents power button off, not undim. + // It is normal to have a short BRIGHT for power button off because + // the system need to play an animation before off. + mHandler.postDelayed(() -> { + mWakefulnessSessionFrameworkStatsLogger.logDimEvent( + mPhysicalDisplayPortIdForDefaultDisplay, + reasonToBeChecked, + savedLastUserActivity, + savedLastUserActivityDurationMs, + savedDimDurationMs, + mScreenOffTimeoutMs); + mPastDimDurationMs = savedDimDurationMs; + }, HANDLER_TOKEN, SCREEN_POLICY_DIM_POWER_OFF_BRIGHT_THRESHOLD_MILLIS); + } + return; + } + case POLICY_REASON_BRIGHT_INITIATED_REVERT: { + // the dimDuration in BRIGHT_INITIATE_REVERT is for the dim duration before + // screen interactive off (mPastDimDurationMs) + long offToOnDurationMs = eventTime - mInteractiveOffTimestamp; + if (mPastDimDurationMs > 0 + && offToOnDurationMs < USER_INITIATED_REVERT_THRESHOLD_MILLIS) { + mWakefulnessSessionFrameworkStatsLogger.logDimEvent( + mPhysicalDisplayPortIdForDefaultDisplay, + reasonToBeChecked, + lastUserActivity, + lastUserActivityDurationMs, + mPastDimDurationMs, + mScreenOffTimeoutMs); + } + return; + } + default: + return; + } + } + void dump(IndentingPrintWriter writer) { final long now = mClock.uptimeMillis(); @@ -475,6 +710,12 @@ public class WakefulnessSessionObserver { final long prevUserActivityDurationMs = now - mPrevUserActivityTimestamp; writer.println("previous user activity duration: " + prevUserActivityDurationMs); writer.println("is in override timeout: " + isInOverrideTimeout()); + writer.println("mIsInteractive: " + mIsInteractive); + writer.println("current screen policy: " + mCurrentScreenPolicy); + final long currentScreenPolicyDurationMs = now - mCurrentScreenPolicyTimestamp; + writer.println("current screen policy duration: " + currentScreenPolicyDurationMs); + writer.println("previous screen policy: " + mPrevScreenPolicy); + writer.println("past screen policy duration: " + mPrevScreenPolicyDurationMs); writer.decreaseIndent(); } } @@ -512,6 +753,24 @@ public class WakefulnessSessionObserver { (long) defaultTimeoutMs); } + public void logDimEvent( + int physicalDisplayPortId, + @PolicyReason int policyReason, + @PowerManager.UserActivityEvent int userActivityEvent, + int lastUserActivityEventDurationMs, + int dimDurationMs, + int defaultTimeoutMs) { + int logUserActivityEvent = convertToLogUserActivityEvent(userActivityEvent); + FrameworkStatsLog.write( + FrameworkStatsLog.SCREEN_DIM_REPORTED, + physicalDisplayPortId, + policyReason, + logUserActivityEvent, + lastUserActivityEventDurationMs, + dimDurationMs, + defaultTimeoutMs); + } + private static final int USER_ACTIVITY_OTHER = FrameworkStatsLog .SCREEN_INTERACTIVE_SESSION_REPORTED__LAST_USER_ACTIVITY_EVENT__OTHER; @@ -591,5 +850,13 @@ public class WakefulnessSessionObserver { Clock getClock() { return SystemClock::uptimeMillis; } + + Handler getHandler() { + return BackgroundThread.getHandler(); + } + + DisplayManagerInternal getDisplayManagerInternal() { + return LocalServices.getService(DisplayManagerInternal.class); + } } } diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index 7e27407fc57f..f6c3d8ef1249 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -76,6 +76,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.PriorityQueue; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.TimeUnit; @@ -109,12 +110,31 @@ public final class HintManagerService extends SystemService { @GuardedBy("mChannelMapLock") private ArrayMap<Integer, TreeMap<Integer, ChannelItem>> mChannelMap; + /* + * Multi-level map storing the session statistics since last pull from StatsD. + * The first level is keyed by the UID of the process owning the session. + * The second level is keyed by the tag of the session. The point of separating different + * tags is that since different categories (e.g. HWUI vs APP) of the sessions may have different + * behaviors. + */ + @GuardedBy("mSessionSnapshotMapLock") + private ArrayMap<Integer, ArrayMap<Integer, AppHintSessionSnapshot>> mSessionSnapshotMap; + /** Lock to protect mActiveSessions and the UidObserver. */ private final Object mLock = new Object(); /** Lock to protect mChannelMap. */ private final Object mChannelMapLock = new Object(); + /* + * Lock to protect mSessionSnapshotMap. + * Nested acquisition of mSessionSnapshotMapLock and mLock should be avoided. + * We should grab these separately. + * When we need to have nested acquisitions, we should always follow the order of acquiring + * mSessionSnapshotMapLock first then mLock. + */ + private final Object mSessionSnapshotMapLock = new Object(); + @GuardedBy("mNonIsolatedTidsLock") private final Map<Integer, Set<Long>> mNonIsolatedTids; @@ -135,6 +155,8 @@ public final class HintManagerService extends SystemService { private int mPowerHalVersion; private final PackageManager mPackageManager; + private boolean mUsesFmq; + private static final String PROPERTY_SF_ENABLE_CPU_HINT = "debug.sf.enable_adpf_cpu_hint"; private static final String PROPERTY_HWUI_ENABLE_HINT_MANAGER = "debug.hwui.use_hint_manager"; @@ -162,6 +184,7 @@ public final class HintManagerService extends SystemService { } mActiveSessions = new ArrayMap<>(); mChannelMap = new ArrayMap<>(); + mSessionSnapshotMap = new ArrayMap<>(); mNativeWrapper = injector.createNativeWrapper(); mNativeWrapper.halInit(); mHintSessionPreferredRate = mNativeWrapper.halGetHintSessionPreferredRate(); @@ -170,6 +193,7 @@ public final class HintManagerService extends SystemService { LocalServices.getService(ActivityManagerInternal.class)); mPowerHal = injector.createIPower(); mPowerHalVersion = 0; + mUsesFmq = false; if (mPowerHal != null) { try { mPowerHalVersion = mPowerHal.getInterfaceVersion(); @@ -197,6 +221,134 @@ public final class HintManagerService extends SystemService { } } + private class AppHintSessionSnapshot { + /* + * Per-Uid and Per-SessionTag snapshot that tracks metrics including + * number of created sessions, number of power efficienct sessions, and + * maximum number of threads in a session. + * Given that it's Per-SessionTag, each uid can have multiple snapshots. + */ + int mCurrentSessionCount; + int mMaxConcurrentSession; + int mMaxThreadCount; + int mPowerEfficientSessionCount; + + final int mTargetDurationNsCountPQSize = 100; + PriorityQueue<TargetDurationRecord> mTargetDurationNsCountPQ; + + class TargetDurationRecord implements Comparable<TargetDurationRecord> { + long mTargetDurationNs; + long mTimestamp; + int mCount; + TargetDurationRecord(long targetDurationNs) { + mTargetDurationNs = targetDurationNs; + mTimestamp = System.currentTimeMillis(); + mCount = 1; + } + + @Override + public int compareTo(TargetDurationRecord t) { + int tCount = t.getCount(); + int thisCount = this.getCount(); + // Here we sort in the order of number of count in ascending order. + // i.e. the lowest count of target duration is at the head of the queue. + // Upon same count, the tiebreaker is the timestamp, the older item will be at the + // front of the queue. + if (tCount == thisCount) { + return (t.getTimestamp() < this.getTimestamp()) ? 1 : -1; + } + return (tCount < thisCount) ? 1 : -1; + } + long getTargetDurationNs() { + return mTargetDurationNs; + } + + int getCount() { + return mCount; + } + + long getTimestamp() { + return mTimestamp; + } + + void setCount(int count) { + mCount = count; + } + + void setTimestamp() { + mTimestamp = System.currentTimeMillis(); + } + + void setTargetDurationNs(long targetDurationNs) { + mTargetDurationNs = targetDurationNs; + } + } + + AppHintSessionSnapshot() { + mCurrentSessionCount = 0; + mMaxConcurrentSession = 0; + mMaxThreadCount = 0; + mPowerEfficientSessionCount = 0; + mTargetDurationNsCountPQ = new PriorityQueue<>(1); + } + + void updateUponSessionCreation(int threadCount, long targetDuration) { + mCurrentSessionCount += 1; + mMaxConcurrentSession = Math.max(mMaxConcurrentSession, mCurrentSessionCount); + mMaxThreadCount = Math.max(mMaxThreadCount, threadCount); + updateTargetDurationNs(targetDuration); + } + + void updateUponSessionClose() { + mCurrentSessionCount -= 1; + } + + void logPowerEfficientSession() { + mPowerEfficientSessionCount += 1; + } + + void updateThreadCount(int threadCount) { + mMaxThreadCount = Math.max(mMaxThreadCount, threadCount); + } + + void updateTargetDurationNs(long targetDurationNs) { + for (TargetDurationRecord t : mTargetDurationNsCountPQ) { + if (t.getTargetDurationNs() == targetDurationNs) { + t.setCount(t.getCount() + 1); + t.setTimestamp(); + return; + } + } + if (mTargetDurationNsCountPQ.size() == mTargetDurationNsCountPQSize) { + mTargetDurationNsCountPQ.poll(); + } + mTargetDurationNsCountPQ.add(new TargetDurationRecord(targetDurationNs)); + } + + int getMaxConcurrentSession() { + return mMaxConcurrentSession; + } + + int getMaxThreadCount() { + return mMaxThreadCount; + } + + int getPowerEfficientSessionCount() { + return mPowerEfficientSessionCount; + } + + long[] targetDurationNsList() { + final int listSize = 5; + long[] targetDurations = new long[listSize]; + while (mTargetDurationNsCountPQ.size() > listSize) { + mTargetDurationNsCountPQ.poll(); + } + for (int i = 0; i < listSize && !mTargetDurationNsCountPQ.isEmpty(); ++i) { + targetDurations[i] = mTargetDurationNsCountPQ.poll().getTargetDurationNs(); + } + return targetDurations; + } + } private boolean isHalSupported() { return mHintSessionPreferredRate != -1; } @@ -235,6 +387,11 @@ public final class HintManagerService extends SystemService { null, // use default PullAtomMetadata values DIRECT_EXECUTOR, this::onPullAtom); + statsManager.setPullAtomCallback( + FrameworkStatsLog.ADPF_SESSION_SNAPSHOT, + null, // use default PullAtomMetadata values + DIRECT_EXECUTOR, + this::onPullAtom); } private int onPullAtom(int atomTag, @NonNull List<StatsEvent> data) { @@ -247,11 +404,82 @@ public final class HintManagerService extends SystemService { data.add(FrameworkStatsLog.buildStatsEvent( FrameworkStatsLog.ADPF_SYSTEM_COMPONENT_INFO, isSurfaceFlingerUsingCpuHint, - isHwuiHintManagerEnabled)); + isHwuiHintManagerEnabled, + getFmqUsage())); + } + if (atomTag == FrameworkStatsLog.ADPF_SESSION_SNAPSHOT) { + synchronized (mSessionSnapshotMapLock) { + for (int i = 0; i < mSessionSnapshotMap.size(); ++i) { + final int uid = mSessionSnapshotMap.keyAt(i); + final ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots = + mSessionSnapshotMap.valueAt(i); + for (int j = 0; j < sessionSnapshots.size(); ++j) { + final int sessionTag = sessionSnapshots.keyAt(j); + final AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.valueAt(j); + data.add(FrameworkStatsLog.buildStatsEvent( + FrameworkStatsLog.ADPF_SESSION_SNAPSHOT, + uid, + sessionTag, + sessionSnapshot.getMaxConcurrentSession(), + sessionSnapshot.getMaxThreadCount(), + sessionSnapshot.getPowerEfficientSessionCount(), + sessionSnapshot.targetDurationNsList() + )); + } + } + } + restoreSessionSnapshot(); } return android.app.StatsManager.PULL_SUCCESS; } + private int getFmqUsage() { + if (mUsesFmq) { + return FrameworkStatsLog.ADPFSYSTEM_COMPONENT_INFO__FMQ_SUPPORTED__SUPPORTED; + } else if (mPowerHalVersion < 5) { + return FrameworkStatsLog.ADPFSYSTEM_COMPONENT_INFO__FMQ_SUPPORTED__HAL_VERSION_NOT_MET; + } else { + return FrameworkStatsLog.ADPFSYSTEM_COMPONENT_INFO__FMQ_SUPPORTED__UNSUPPORTED; + } + } + + private void restoreSessionSnapshot() { + // clean up snapshot map and rebuild with current active sessions + synchronized (mSessionSnapshotMapLock) { + mSessionSnapshotMap.clear(); + synchronized (mLock) { + for (int i = 0; i < mActiveSessions.size(); i++) { + ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap = + mActiveSessions.valueAt(i); + for (int j = 0; j < tokenMap.size(); j++) { + ArraySet<AppHintSession> sessionSet = tokenMap.valueAt(j); + for (int k = 0; k < sessionSet.size(); ++k) { + AppHintSession appHintSession = sessionSet.valueAt(k); + final int tag = appHintSession.getTag(); + final int uid = appHintSession.getUid(); + final long targetDuationNs = + appHintSession.getTargetDurationNs(); + final int threadCount = appHintSession.getThreadIds().length; + ArrayMap<Integer, AppHintSessionSnapshot> snapshots = + mSessionSnapshotMap.get(uid); + if (snapshots == null) { + snapshots = new ArrayMap<>(); + mSessionSnapshotMap.put(uid, snapshots); + } + AppHintSessionSnapshot snapshot = snapshots.get(tag); + if (snapshot == null) { + snapshot = new AppHintSessionSnapshot(); + snapshots.put(tag, snapshot); + } + snapshot.updateUponSessionCreation(threadCount, + targetDuationNs); + } + } + } + } + } + } + /** * Wrapper around the static-native methods from native. * @@ -833,17 +1061,13 @@ public final class HintManagerService extends SystemService { // we change the session tag to SessionTag.GAME // as it was not previously classified switch (getUidApplicationCategory(callingUid)) { - case ApplicationInfo.CATEGORY_GAME: - tag = SessionTag.GAME; - break; - case ApplicationInfo.CATEGORY_UNDEFINED: + case ApplicationInfo.CATEGORY_GAME -> tag = SessionTag.GAME; + case ApplicationInfo.CATEGORY_UNDEFINED -> // We use CATEGORY_UNDEFINED to filter the case when // PackageManager.NameNotFoundException is caught, // which should not happen. tag = SessionTag.APP; - break; - default: - tag = SessionTag.APP; + default -> tag = SessionTag.APP; } } @@ -889,9 +1113,15 @@ public final class HintManagerService extends SystemService { logPerformanceHintSessionAtom( callingUid, sessionId, durationNanos, tids, tag); + synchronized (mSessionSnapshotMapLock) { + // Update session snapshot upon session creation + mSessionSnapshotMap.computeIfAbsent(callingUid, k -> new ArrayMap<>()) + .computeIfAbsent(tag, k -> new AppHintSessionSnapshot()) + .updateUponSessionCreation(tids.length, durationNanos); + } synchronized (mLock) { - AppHintSession hs = new AppHintSession(callingUid, callingTgid, tids, token, - halSessionPtr, durationNanos); + AppHintSession hs = new AppHintSession(callingUid, callingTgid, tag, tids, + token, halSessionPtr, durationNanos); ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap = mActiveSessions.get(callingUid); if (tokenMap == null) { @@ -904,6 +1134,8 @@ public final class HintManagerService extends SystemService { tokenMap.put(token, sessionSet); } sessionSet.add(hs); + mUsesFmq = mUsesFmq || hasChannel(callingTgid, callingUid); + return hs; } } finally { @@ -996,6 +1228,7 @@ public final class HintManagerService extends SystemService { final class AppHintSession extends IHintSession.Stub implements IBinder.DeathRecipient { protected final int mUid; protected final int mPid; + protected final int mTag; protected int[] mThreadIds; protected final IBinder mToken; protected long mHalSessionPtr; @@ -1003,6 +1236,7 @@ public final class HintManagerService extends SystemService { protected boolean mUpdateAllowedByProcState; protected int[] mNewThreadIds; protected boolean mPowerEfficient; + protected boolean mHasBeenPowerEfficient; protected boolean mShouldForcePause; private enum SessionModes { @@ -1010,16 +1244,18 @@ public final class HintManagerService extends SystemService { }; protected AppHintSession( - int uid, int pid, int[] threadIds, IBinder token, + int uid, int pid, int sessionTag, int[] threadIds, IBinder token, long halSessionPtr, long durationNanos) { mUid = uid; mPid = pid; + mTag = sessionTag; mToken = token; mThreadIds = threadIds; mHalSessionPtr = halSessionPtr; mTargetDurationNanos = durationNanos; mUpdateAllowedByProcState = true; mPowerEfficient = false; + mHasBeenPowerEfficient = false; mShouldForcePause = false; final boolean allowed = mUidObserver.isUidForeground(mUid); updateHintAllowedByProcState(allowed); @@ -1056,6 +1292,20 @@ public final class HintManagerService extends SystemService { mNativeWrapper.halUpdateTargetWorkDuration(mHalSessionPtr, targetDurationNanos); mTargetDurationNanos = targetDurationNanos; } + synchronized (mSessionSnapshotMapLock) { + ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots = + mSessionSnapshotMap.get(mUid); + if (sessionSnapshots == null) { + Slogf.w(TAG, "Session snapshot map is null for uid " + mUid); + return; + } + AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag); + if (sessionSnapshot == null) { + Slogf.w(TAG, "Session snapshot is null for uid " + mUid + " and tag " + mTag); + return; + } + sessionSnapshot.updateTargetDurationNs(mTargetDurationNanos); + } } @Override @@ -1108,6 +1358,20 @@ public final class HintManagerService extends SystemService { if (sessionSet.isEmpty()) tokenMap.remove(mToken); if (tokenMap.isEmpty()) mActiveSessions.remove(mUid); } + synchronized (mSessionSnapshotMapLock) { + ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots = + mSessionSnapshotMap.get(mUid); + if (sessionSnapshots == null) { + Slogf.w(TAG, "Session snapshot map is null for uid " + mUid); + return; + } + AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag); + if (sessionSnapshot == null) { + Slogf.w(TAG, "Session snapshot is null for uid " + mUid + " and tag " + mTag); + return; + } + sessionSnapshot.updateUponSessionClose(); + } if (powerhintThreadCleanup()) { synchronized (mNonIsolatedTidsLock) { final int[] tids = getTidsInternal(); @@ -1191,6 +1455,21 @@ public final class HintManagerService extends SystemService { mShouldForcePause = false; } } + synchronized (mSessionSnapshotMapLock) { + ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots = + mSessionSnapshotMap.get(mUid); + if (sessionSnapshots == null) { + Slogf.w(TAG, "Session snapshot map is null for uid " + mUid); + return; + } + AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag); + if (sessionSnapshot == null) { + Slogf.w(TAG, "Session snapshot is null for uid " + mUid + " and tag " + + mTag); + return; + } + sessionSnapshot.updateThreadCount(tids.length); + } } public int[] getThreadIds() { @@ -1231,6 +1510,26 @@ public final class HintManagerService extends SystemService { } mNativeWrapper.halSetMode(mHalSessionPtr, mode, enabled); } + if (enabled && (mode == SessionModes.POWER_EFFICIENCY.ordinal())) { + if (!mHasBeenPowerEfficient) { + mHasBeenPowerEfficient = true; + synchronized (mSessionSnapshotMapLock) { + ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots = + mSessionSnapshotMap.get(mUid); + if (sessionSnapshots == null) { + Slogf.w(TAG, "Session snapshot map is null for uid " + mUid); + return; + } + AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag); + if (sessionSnapshot == null) { + Slogf.w(TAG, "Session snapshot is null for uid " + mUid + + " and tag " + mTag); + return; + } + sessionSnapshot.logPowerEfficientSession(); + } + } + } } @Override @@ -1254,6 +1553,20 @@ public final class HintManagerService extends SystemService { } } + public int getUid() { + return mUid; + } + + public int getTag() { + return mTag; + } + + public long getTargetDurationNs() { + synchronized (this) { + return mTargetDurationNanos; + } + } + void validateWorkDuration(WorkDuration workDuration) { if (DEBUG) { Slogf.d(TAG, "WorkDuration(" diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java index 6d519ee200c2..90981ada7c31 100644 --- a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java +++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java @@ -86,8 +86,7 @@ public class WifiPowerStatsCollector extends PowerStatsCollector { private ConsumedEnergyRetriever mConsumedEnergyRetriever; private IntSupplier mVoltageSupplier; private int[] mEnergyConsumerIds = new int[0]; - private WifiActivityEnergyInfo mLastWifiActivityInfo = - new WifiActivityEnergyInfo(0, 0, 0, 0, 0, 0); + private WifiActivityEnergyInfo mLastWifiActivityInfo; private NetworkStats mLastNetworkStats; private long[] mLastConsumedEnergyUws; private int mLastVoltageMv; @@ -206,14 +205,21 @@ public class WifiPowerStatsCollector extends PowerStatsCollector { return null; } - long rxDuration = activityInfo.getControllerRxDurationMillis() - - mLastWifiActivityInfo.getControllerRxDurationMillis(); - long txDuration = activityInfo.getControllerTxDurationMillis() - - mLastWifiActivityInfo.getControllerTxDurationMillis(); - long scanDuration = activityInfo.getControllerScanDurationMillis() - - mLastWifiActivityInfo.getControllerScanDurationMillis(); - long idleDuration = activityInfo.getControllerIdleDurationMillis() - - mLastWifiActivityInfo.getControllerIdleDurationMillis(); + long rxDuration = 0; + long txDuration = 0; + long scanDuration = 0; + long idleDuration = 0; + + if (mLastWifiActivityInfo != null) { + rxDuration = activityInfo.getControllerRxDurationMillis() + - mLastWifiActivityInfo.getControllerRxDurationMillis(); + txDuration = activityInfo.getControllerTxDurationMillis() + - mLastWifiActivityInfo.getControllerTxDurationMillis(); + scanDuration = activityInfo.getControllerScanDurationMillis() + - mLastWifiActivityInfo.getControllerScanDurationMillis(); + idleDuration = activityInfo.getControllerIdleDurationMillis() + - mLastWifiActivityInfo.getControllerIdleDurationMillis(); + } mLayout.setDeviceRxTime(mDeviceStats, rxDuration); mLayout.setDeviceTxTime(mDeviceStats, txDuration); diff --git a/services/core/java/com/android/server/updates/Android.bp b/services/core/java/com/android/server/updates/Android.bp new file mode 100644 index 000000000000..10beebb82711 --- /dev/null +++ b/services/core/java/com/android/server/updates/Android.bp @@ -0,0 +1,11 @@ +aconfig_declarations { + name: "updates_flags", + package: "com.android.server.updates", + container: "system", + srcs: ["*.aconfig"], +} + +java_aconfig_library { + name: "updates_flags_lib", + aconfig_declarations: "updates_flags", +} diff --git a/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java b/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java index 5565b6ffb5ac..af4025e1db7c 100644 --- a/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java +++ b/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java @@ -16,17 +16,15 @@ package com.android.server.updates; +import android.content.Context; +import android.content.Intent; import android.os.FileUtils; import android.system.ErrnoException; import android.system.Os; -import android.util.Base64; import android.util.Slog; -import com.android.internal.util.HexDump; - import libcore.io.Streams; -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -36,10 +34,7 @@ import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInstallReceiver { @@ -52,31 +47,31 @@ public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInsta @Override protected void install(InputStream inputStream, int version) throws IOException { - /* Install is complicated here because we translate the input, which is a JSON file - * containing log information to a directory with a file per log. To support atomically - * replacing the old configuration directory with the new there's a bunch of steps. We - * create a new directory with the logs and then do an atomic update of the current symlink - * to point to the new directory. - */ + if (!Flags.certificateTransparencyInstaller()) { + return; + } + // To support atomically replacing the old configuration directory with the new there's a + // bunch of steps. We create a new directory with the logs and then do an atomic update of + // the current symlink to point to the new directory. // 1. Ensure that the update dir exists and is readable updateDir.mkdir(); if (!updateDir.isDirectory()) { throw new IOException("Unable to make directory " + updateDir.getCanonicalPath()); } if (!updateDir.setReadable(true, false)) { - throw new IOException("Unable to set permissions on " + - updateDir.getCanonicalPath()); + throw new IOException("Unable to set permissions on " + updateDir.getCanonicalPath()); } File currentSymlink = new File(updateDir, "current"); File newVersion = new File(updateDir, LOGDIR_PREFIX + String.valueOf(version)); - File oldDirectory; // 2. Handle the corner case where the new directory already exists. if (newVersion.exists()) { // If the symlink has already been updated then the update died between steps 7 and 8 // and so we cannot delete the directory since its in use. Instead just bump the version // and return. if (newVersion.getCanonicalPath().equals(currentSymlink.getCanonicalPath())) { - writeUpdate(updateDir, updateVersion, + writeUpdate( + updateDir, + updateVersion, new ByteArrayInputStream(Long.toString(version).getBytes())); deleteOldLogDirectories(); return; @@ -91,22 +86,12 @@ public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInsta throw new IOException("Unable to make directory " + newVersion.getCanonicalPath()); } if (!newVersion.setReadable(true, false)) { - throw new IOException("Failed to set " +newVersion.getCanonicalPath() + - " readable"); + throw new IOException( + "Failed to set " + newVersion.getCanonicalPath() + " readable"); } - // 4. For each log in the log file create the corresponding file in <new_version>/ . - try { - byte[] content = Streams.readFullyNoClose(inputStream); - JSONObject json = new JSONObject(new String(content, StandardCharsets.UTF_8)); - JSONArray logs = json.getJSONArray("logs"); - for (int i = 0; i < logs.length(); i++) { - JSONObject log = logs.getJSONObject(i); - installLog(newVersion, log); - } - } catch (JSONException e) { - throw new IOException("Failed to parse logs", e); - } + // 4. Validate the log list json and move the file in <new_version>/ . + installLogList(newVersion, inputStream); // 5. Create the temp symlink. We'll rename this to the target symlink to get an atomic // update. @@ -125,62 +110,53 @@ public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInsta } Slog.i(TAG, "CT log directory updated to " + newVersion.getAbsolutePath()); // 7. Update the current version information - writeUpdate(updateDir, updateVersion, + writeUpdate( + updateDir, + updateVersion, new ByteArrayInputStream(Long.toString(version).getBytes())); // 8. Cleanup deleteOldLogDirectories(); } - private void installLog(File directory, JSONObject logObject) throws IOException { + @Override + protected void postInstall(Context context, Intent intent) { + if (!Flags.certificateTransparencyInstaller()) { + return; + } + } + + private void installLogList(File directory, InputStream inputStream) throws IOException { try { - String logFilename = getLogFileName(logObject.getString("key")); - File file = new File(directory, logFilename); - try (OutputStreamWriter out = - new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { - writeLogEntry(out, "key", logObject.getString("key")); - writeLogEntry(out, "url", logObject.getString("url")); - writeLogEntry(out, "description", logObject.getString("description")); + byte[] content = Streams.readFullyNoClose(inputStream); + if (new JSONObject(new String(content, StandardCharsets.UTF_8)).length() == 0) { + throw new IOException("Log list data not valid"); + } + + File file = new File(directory, "log_list.json"); + try (FileOutputStream outputStream = new FileOutputStream(file)) { + outputStream.write(content); } if (!file.setReadable(true, false)) { throw new IOException("Failed to set permissions on " + file.getCanonicalPath()); } } catch (JSONException e) { - throw new IOException("Failed to parse log", e); - } - - } - - /** - * Get the filename for a log based on its public key. This must be kept in sync with - * org.conscrypt.ct.CTLogStoreImpl. - */ - private String getLogFileName(String base64PublicKey) { - byte[] keyBytes = Base64.decode(base64PublicKey, Base64.DEFAULT); - try { - byte[] id = MessageDigest.getInstance("SHA-256").digest(keyBytes); - return HexDump.toHexString(id, false); - } catch (NoSuchAlgorithmException e) { - // SHA-256 is guaranteed to be available. - throw new RuntimeException(e); + throw new IOException("Malformed json in log list", e); } } - private void writeLogEntry(OutputStreamWriter out, String key, String value) - throws IOException { - out.write(key + ":" + value + "\n"); - } - private void deleteOldLogDirectories() throws IOException { if (!updateDir.exists()) { return; } File currentTarget = new File(updateDir, "current").getCanonicalFile(); - FileFilter filter = new FileFilter() { - @Override - public boolean accept(File file) { - return !currentTarget.equals(file) && file.getName().startsWith(LOGDIR_PREFIX); - } - }; + FileFilter filter = + new FileFilter() { + @Override + public boolean accept(File file) { + return !currentTarget.equals(file) + && file.getName().startsWith(LOGDIR_PREFIX); + } + }; for (File f : updateDir.listFiles(filter)) { FileUtils.deleteContentsAndDir(f); } diff --git a/services/core/java/com/android/server/updates/flags.aconfig b/services/core/java/com/android/server/updates/flags.aconfig new file mode 100644 index 000000000000..476cb3723c97 --- /dev/null +++ b/services/core/java/com/android/server/updates/flags.aconfig @@ -0,0 +1,10 @@ +package: "com.android.server.updates" +container: "system" + +flag { + name: "certificate_transparency_installer" + is_exported: true + namespace: "network_security" + description: "Enable certificate transparency installer for log list data" + bug: "319829948" +} diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 5be5bc5e3952..2c734127b7ea 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -47,7 +47,7 @@ import static com.android.server.accessibility.AccessibilityTraceProto.WHERE; import static com.android.server.accessibility.AccessibilityTraceProto.WINDOW_MANAGER_SERVICE; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; -import static com.android.server.wm.WindowTracing.WINSCOPE_EXT; +import static com.android.server.wm.WindowTracingLegacy.WINSCOPE_EXT; import android.accessibilityservice.AccessibilityTrace; import android.animation.ObjectAnimator; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b5c12ce64e14..876807485eca 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -64,6 +64,7 @@ import static android.content.Intent.CATEGORY_SECONDARY_HOME; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY; import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; +import static android.content.pm.ActivityInfo.CONFIG_RESOURCES_UNUSED; import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT; import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; @@ -107,7 +108,6 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; import static android.content.res.Configuration.UI_MODE_TYPE_DESK; import static android.content.res.Configuration.UI_MODE_TYPE_MASK; -import static android.content.res.Configuration.UI_MODE_TYPE_VR_HEADSET; import static android.os.Build.VERSION_CODES.HONEYCOMB; import static android.os.Build.VERSION_CODES.O; import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; @@ -233,11 +233,10 @@ import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_F import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE; import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE; import static com.android.server.wm.ActivityTaskManagerService.getInputDispatchingTimeoutMillisLocked; +import static com.android.server.wm.DesktopModeLaunchParamsModifier.canEnterDesktopMode; import static com.android.server.wm.IdentifierProto.HASH_CODE; import static com.android.server.wm.IdentifierProto.TITLE; import static com.android.server.wm.IdentifierProto.USER_ID; -import static com.android.server.wm.LetterboxConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; -import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIENT; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; @@ -477,9 +476,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // finished destroying itself. private static final int DESTROY_TIMEOUT = 10 * 1000; - // Rounding tolerance to be used in aspect ratio computations - private static final float ASPECT_RATIO_ROUNDING_TOLERANCE = 0.005f; - final ActivityTaskManagerService mAtmService; final ActivityCallerState mCallerState; @NonNull @@ -823,26 +819,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // naturally. private boolean mInSizeCompatModeForBounds = false; - // Whether the aspect ratio restrictions applied to the activity bounds in applyAspectRatio(). - private boolean mIsAspectRatioApplied = false; - - // Bounds populated in resolveFixedOrientationConfiguration when this activity is letterboxed - // for fixed orientation. If not null, they are used as parent container in - // resolveSizeCompatModeConfiguration and in a constructor of CompatDisplayInsets. If - // letterboxed due to fixed orientation then aspect ratio restrictions are also respected. - // This happens when an activity has fixed orientation which doesn't match orientation of the - // parent because a display is ignoring orientation request or fixed to user rotation. - // See WindowManagerService#getIgnoreOrientationRequest and - // WindowManagerService#getFixedToUserRotation for more context. - @Nullable - private Rect mLetterboxBoundsForFixedOrientationAndAspectRatio; - - // Bounds populated in resolveAspectRatioRestriction when this activity is letterboxed for - // aspect ratio. If not null, they are used as parent container in - // resolveSizeCompatModeConfiguration and in a constructor of CompatDisplayInsets. - @Nullable - private Rect mLetterboxBoundsForAspectRatio; - // Whether the activity is eligible to be letterboxed for fixed orientation with respect to its // requested orientation, even when it's letterbox for another reason (e.g., size compat mode) // and therefore #isLetterboxedForFixedOrientationAndAspectRatio returns false. @@ -884,8 +860,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** The last set {@link DropInputMode} for this activity surface. */ @DropInputMode private int mLastDropInputMode = DropInputMode.NONE; - /** Whether the input to this activity will be dropped during the current playing animation. */ - private boolean mIsInputDroppedForAnimation; /** * Whether the application has desk mode resources. Calculated and cached when @@ -1128,11 +1102,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A pw.println(prefix + "mLastReportedConfigurations:"); mLastReportedConfiguration.dump(pw, prefix + " "); - if (Flags.activityWindowInfoFlag()) { - pw.print(prefix); - pw.print("mLastReportedActivityWindowInfo="); - pw.println(mLastReportedActivityWindowInfo); - } + pw.print(prefix); + pw.print("mLastReportedActivityWindowInfo="); + pw.println(mLastReportedActivityWindowInfo); pw.print(prefix); pw.print("CurrentConfiguration="); pw.println(getConfiguration()); if (!getRequestedOverrideConfiguration().equals(EMPTY)) { @@ -1725,15 +1697,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } - /** Sets if all input will be dropped as a protection during the client-driven animation. */ - void setDropInputForAnimation(boolean isInputDroppedForAnimation) { - if (mIsInputDroppedForAnimation == isInputDroppedForAnimation) { - return; - } - mIsInputDroppedForAnimation = isInputDroppedForAnimation; - updateUntrustedEmbeddingInputProtection(); - } - /** * Sets to drop input when obscured to activity if it is embedded in untrusted mode. * @@ -1746,10 +1709,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (getSurfaceControl() == null) { return; } - if (mIsInputDroppedForAnimation) { - // Disable all input during the animation. - setDropInputMode(DropInputMode.ALL); - } else if (isEmbeddedInUntrustedMode()) { + if (isEmbeddedInUntrustedMode()) { // Set drop input to OBSCURED when untrusted embedded. setDropInputMode(DropInputMode.OBSCURED); } else { @@ -3199,7 +3159,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @NonNull ActivityWindowInfo getActivityWindowInfo() { - if (!Flags.activityWindowInfoFlag() || !isAttached()) { + if (!isAttached()) { return mTmpActivityWindowInfo; } if (isFixedRotationTransforming()) { @@ -8322,9 +8282,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } void setLastReportedActivityWindowInfo(@NonNull ActivityWindowInfo activityWindowInfo) { - if (Flags.activityWindowInfoFlag()) { - mLastReportedActivityWindowInfo.set(activityWindowInfo); - } + mLastReportedActivityWindowInfo.set(activityWindowInfo); } @Nullable @@ -8384,7 +8342,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * aspect ratio. */ boolean shouldCreateCompatDisplayInsets() { - if (mLetterboxUiController.hasFullscreenOverride()) { + if (mAppCompatController.getAppCompatAspectRatioOverrides().hasFullscreenOverride()) { // If the user has forced the applications aspect ratio to be fullscreen, don't use size // compatibility mode in any situation. The user has been warned and therefore accepts // the risk of the application misbehaving. @@ -8473,10 +8431,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A fullConfig.windowConfiguration.getRotation()); } - final Rect letterboxedContainerBounds = - mLetterboxBoundsForFixedOrientationAndAspectRatio != null - ? mLetterboxBoundsForFixedOrientationAndAspectRatio - : mLetterboxBoundsForAspectRatio; + final Rect letterboxedContainerBounds = mAppCompatController + .getAppCompatAspectRatioPolicy().getLetterboxedContainerBounds(); + // The role of CompatDisplayInsets is like the override bounds. mCompatDisplayInsets = new CompatDisplayInsets( @@ -8550,10 +8507,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A newParentConfiguration = mTmpConfig; } - mIsAspectRatioApplied = false; + mAppCompatController.getAppCompatAspectRatioPolicy().reset(); mIsEligibleForFixedOrientationLetterbox = false; - mLetterboxBoundsForFixedOrientationAndAspectRatio = null; - mLetterboxBoundsForAspectRatio = null; mResolveConfigHint.resolveTmpOverrides(mDisplayContent, newParentConfiguration, isFixedRotationTransforming()); @@ -8586,8 +8541,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds // are already calculated in resolveFixedOrientationConfiguration. // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer. - if (Flags.immersiveAppRepositioning() && !isLetterboxedForFixedOrientationAndAspectRatio() - && !mLetterboxUiController.hasFullscreenOverride()) { + if (Flags.immersiveAppRepositioning() + && !mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio() + && !mAppCompatController.getAppCompatAspectRatioOverrides() + .hasFullscreenOverride()) { resolveAspectRatioRestriction(newParentConfiguration); } final CompatDisplayInsets compatDisplayInsets = getCompatDisplayInsets(); @@ -8607,8 +8565,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // are already calculated in resolveFixedOrientationConfiguration, or if in size compat // mode, it should already be calculated in resolveSizeCompatModeConfiguration. // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer. - if (!Flags.immersiveAppRepositioning() && !isLetterboxedForFixedOrientationAndAspectRatio() - && !mInSizeCompatModeForBounds && !mLetterboxUiController.hasFullscreenOverride()) { + if (!Flags.immersiveAppRepositioning() + && !mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio() + && !mInSizeCompatModeForBounds + && !mAppCompatController.getAppCompatAspectRatioOverrides() + .hasFullscreenOverride()) { resolveAspectRatioRestriction(newParentConfiguration); } @@ -8625,14 +8587,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Fixed orientation letterboxing is possible on both large screen devices // with ignoreOrientationRequest enabled and on phones in split screen even with // ignoreOrientationRequest disabled. - && (mLetterboxBoundsForFixedOrientationAndAspectRatio != null + && (mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio() // Limiting check for aspect ratio letterboxing to devices with enabled // ignoreOrientationRequest. This avoids affecting phones where apps may // not expect the change of smallestScreenWidthDp after rotation which is // possible with this logic. Not having smallestScreenWidthDp completely // accurate on phones shouldn't make the big difference and is expected // to be already well-tested by apps. - || (isIgnoreOrientationRequest && mIsAspectRatioApplied))) { + || (isIgnoreOrientationRequest + && mAppCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied()))) { // TODO(b/264034555): Use mDisplayContent to calculate smallestScreenWidthDp from all // rotations and only re-calculate if parent bounds have non-orientation size change. resolvedConfig.smallestScreenWidthDp = @@ -8749,11 +8713,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // letterboxed for fixed orientation. Aspect ratio restrictions are also applied if // present. But this doesn't return true when the activity is letterboxed only because // of aspect ratio restrictions. - if (isLetterboxedForFixedOrientationAndAspectRatio()) { + if (mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()) { return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION; } // Letterbox for limited aspect ratio. - if (isLetterboxedForAspectRatioOnly()) { + if (mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForAspectRatioOnly()) { return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO; } @@ -8904,26 +8870,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } /** - * Whether this activity is letterboxed for fixed orientation. If letterboxed due to fixed - * orientation then aspect ratio restrictions are also already respected. - * - * <p>This happens when an activity has fixed orientation which doesn't match orientation of the - * parent because a display setting 'ignoreOrientationRequest' is set to true. See {@link - * WindowManagerService#getIgnoreOrientationRequest} for more context. - */ - boolean isLetterboxedForFixedOrientationAndAspectRatio() { - return mLetterboxBoundsForFixedOrientationAndAspectRatio != null; - } - - boolean isLetterboxedForAspectRatioOnly() { - return mLetterboxBoundsForAspectRatio != null; - } - - boolean isAspectRatioApplied() { - return mIsAspectRatioApplied; - } - - /** * Whether this activity is eligible for letterbox eduction. * * <p>Conditions that need to be met: @@ -8955,7 +8901,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * @param parentBounds are the new parent bounds passed down to the activity and should be used * to compute the stable bounds. * @param outStableBounds will store the stable bounds, which are the bounds with insets - * applied, if orientation is not respected when insets are applied. + * applied, if orientation is not respected when insets are applied.g * Stable bounds should be used to compute letterboxed bounds if * orientation is not respected when insets are applied. * @param outNonDecorBounds will store the non decor bounds, which are the bounds with non @@ -9108,23 +9054,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final Rect prevResolvedBounds = new Rect(resolvedBounds); resolvedBounds.set(containingBounds); - final float letterboxAspectRatioOverride = - mLetterboxUiController.getFixedOrientationLetterboxAspectRatio(newParentConfig); - - // Aspect ratio as suggested by the system. Apps requested mix/max aspect ratio will - // be respected in #applyAspectRatio. - final float desiredAspectRatio; - if (isDefaultMultiWindowLetterboxAspectRatioDesired(newParentConfig)) { - desiredAspectRatio = DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; - } else if (letterboxAspectRatioOverride > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) { - desiredAspectRatio = letterboxAspectRatioOverride; - } else { - desiredAspectRatio = computeAspectRatio(parentBounds); - } - - // Apply aspect ratio to resolved bounds - mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, containingBoundsWithInsets, - containingBounds, desiredAspectRatio); + mAppCompatController.getAppCompatAspectRatioPolicy() + .applyDesiredAspectRatio(newParentConfig, parentBounds, resolvedBounds, + containingBoundsWithInsets, containingBounds); if (compatDisplayInsets != null) { compatDisplayInsets.getBoundsByRotation(mTmpBounds, @@ -9152,21 +9084,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // for comparison with size compat app bounds in {@link resolveSizeCompatModeConfiguration}. mResolveConfigHint.mTmpCompatInsets = compatDisplayInsets; computeConfigByResolveHint(getResolvedOverrideConfiguration(), newParentConfig); - mLetterboxBoundsForFixedOrientationAndAspectRatio = new Rect(resolvedBounds); - } - - /** - * Returns {@code true} if the default aspect ratio for a letterboxed app in multi-window mode - * should be used. - */ - private boolean isDefaultMultiWindowLetterboxAspectRatioDesired( - @NonNull Configuration parentConfig) { - if (mDisplayContent == null) { - return false; - } - final int windowingMode = parentConfig.windowConfiguration.getWindowingMode(); - return WindowConfiguration.inMultiWindowMode(windowingMode) - && !mDisplayContent.getIgnoreOrientationRequest(); + mAppCompatController.getAppCompatAspectRatioPolicy() + .setLetterboxBoundsForFixedOrientationAndAspectRatio(new Rect(resolvedBounds)); } /** @@ -9183,7 +9102,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Use tmp bounds to calculate aspect ratio so we can know whether the activity should use // restricted size (resolved bounds may be the requested override bounds). mTmpBounds.setEmpty(); - mIsAspectRatioApplied = applyAspectRatio(mTmpBounds, parentAppBounds, parentBounds); + mAppCompatController.getAppCompatAspectRatioPolicy() + .applyAspectRatioForLetterbox(mTmpBounds, parentAppBounds, parentBounds); // If the out bounds is not empty, it means the activity cannot fill parent's app bounds, // then they should be aligned later in #updateResolvedBoundsPosition() if (!mTmpBounds.isEmpty()) { @@ -9194,7 +9114,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // restrict, the bounds should be the requested override bounds. mResolveConfigHint.mTmpOverrideDisplayInfo = getFixedRotationTransformDisplayInfo(); computeConfigByResolveHint(resolvedConfig, newParentConfiguration); - mLetterboxBoundsForAspectRatio = new Rect(resolvedBounds); + mAppCompatController.getAppCompatAspectRatioPolicy() + .setLetterboxBoundsForAspectRatio(new Rect(resolvedBounds)); } } @@ -9212,7 +9133,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // activity will be displayed within them even if it is in size compat mode. They should be // saved here before resolved bounds are overridden below. final boolean useResolvedBounds = Flags.immersiveAppRepositioning() - ? isAspectRatioApplied() : isLetterboxedForFixedOrientationAndAspectRatio(); + ? mAppCompatController.getAppCompatAspectRatioPolicy() + .isAspectRatioApplied() + : mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio(); final Rect containerBounds = useResolvedBounds ? new Rect(resolvedBounds) : newParentConfiguration.windowConfiguration.getBounds(); @@ -9256,8 +9180,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A resolvedBounds.set(containingBounds); // The size of floating task is fixed (only swap), so the aspect ratio is already correct. if (!compatDisplayInsets.mIsFloating) { - mIsAspectRatioApplied = - applyAspectRatio(resolvedBounds, containingAppBounds, containingBounds); + mAppCompatController.getAppCompatAspectRatioPolicy() + .applyAspectRatioForLetterbox(resolvedBounds, containingAppBounds, + containingBounds); } // Use resolvedBounds to compute other override configurations such as appBounds. The bounds @@ -9342,18 +9267,24 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } void updateSizeCompatScale(Rect resolvedAppBounds, Rect containerAppBounds) { - // Only allow to scale down. mSizeCompatScale = mAppCompatController.getTransparentPolicy() .findOpaqueNotFinishingActivityBelow() .map(activityRecord -> activityRecord.mSizeCompatScale) - .orElseGet(() -> { - final int contentW = resolvedAppBounds.width(); - final int contentH = resolvedAppBounds.height(); - final int viewportW = containerAppBounds.width(); - final int viewportH = containerAppBounds.height(); - return (contentW <= viewportW && contentH <= viewportH) ? 1f : Math.min( - (float) viewportW / contentW, (float) viewportH / contentH); - }); + .orElseGet(() -> calculateSizeCompatScale(resolvedAppBounds, containerAppBounds)); + } + + private float calculateSizeCompatScale(Rect resolvedAppBounds, Rect containerAppBounds) { + final int contentW = resolvedAppBounds.width(); + final int contentH = resolvedAppBounds.height(); + final int viewportW = containerAppBounds.width(); + final int viewportH = containerAppBounds.height(); + // Allow an application to be up-scaled if its window is smaller than its + // original container or if it's a freeform window in desktop mode. + boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH) + || (canEnterDesktopMode(mAtmService.mContext) + && getWindowingMode() == WINDOWING_MODE_FREEFORM); + return shouldAllowUpscaling ? Math.min( + (float) viewportW / contentW, (float) viewportH / contentH) : 1f; } private boolean isInSizeCompatModeForBounds(final Rect appBounds, final Rect containerBounds) { @@ -9661,125 +9592,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return mPauseConfigurationDispatchCount > 0; } - private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, - Rect containingBounds) { - return applyAspectRatio(outBounds, containingAppBounds, containingBounds, - 0 /* desiredAspectRatio */); - } - - /** - * Applies aspect ratio restrictions to outBounds. If no restrictions, then no change is - * made to outBounds. - * - * @return {@code true} if aspect ratio restrictions were applied. - */ - // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer. - private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, - Rect containingBounds, float desiredAspectRatio) { - final float maxAspectRatio = getMaxAspectRatio(); - final Task rootTask = getRootTask(); - final float minAspectRatio = getMinAspectRatio(); - final TaskFragment organizedTf = getOrganizedTaskFragment(); - float aspectRatioToApply = desiredAspectRatio; - if (task == null || rootTask == null - || (maxAspectRatio < 1 && minAspectRatio < 1 && aspectRatioToApply < 1) - // Don't set aspect ratio if we are in VR mode. - || isInVrUiMode(getConfiguration()) - // TODO(b/232898850): Always respect aspect ratio requests. - // Don't set aspect ratio for activity in ActivityEmbedding split. - || (organizedTf != null && !organizedTf.fillsParent())) { - return false; - } - - final int containingAppWidth = containingAppBounds.width(); - final int containingAppHeight = containingAppBounds.height(); - final float containingRatio = computeAspectRatio(containingAppBounds); - - if (aspectRatioToApply < 1) { - aspectRatioToApply = containingRatio; - } - - if (maxAspectRatio >= 1 && aspectRatioToApply > maxAspectRatio) { - aspectRatioToApply = maxAspectRatio; - } else if (minAspectRatio >= 1 && aspectRatioToApply < minAspectRatio) { - aspectRatioToApply = minAspectRatio; - } - - int activityWidth = containingAppWidth; - int activityHeight = containingAppHeight; - - if (containingRatio - aspectRatioToApply > ASPECT_RATIO_ROUNDING_TOLERANCE) { - if (containingAppWidth < containingAppHeight) { - // Width is the shorter side, so we use that to figure-out what the max. height - // should be given the aspect ratio. - activityHeight = (int) ((activityWidth * aspectRatioToApply) + 0.5f); - } else { - // Height is the shorter side, so we use that to figure-out what the max. width - // should be given the aspect ratio. - activityWidth = (int) ((activityHeight * aspectRatioToApply) + 0.5f); - } - } else if (aspectRatioToApply - containingRatio > ASPECT_RATIO_ROUNDING_TOLERANCE) { - boolean adjustWidth; - switch (getRequestedConfigurationOrientation()) { - case ORIENTATION_LANDSCAPE: - // Width should be the longer side for this landscape app, so we use the width - // to figure-out what the max. height should be given the aspect ratio. - adjustWidth = false; - break; - case ORIENTATION_PORTRAIT: - // Height should be the longer side for this portrait app, so we use the height - // to figure-out what the max. width should be given the aspect ratio. - adjustWidth = true; - break; - default: - // This app doesn't have a preferred orientation, so we keep the length of the - // longer side, and use it to figure-out the length of the shorter side. - if (containingAppWidth < containingAppHeight) { - // Width is the shorter side, so we use the height to figure-out what the - // max. width should be given the aspect ratio. - adjustWidth = true; - } else { - // Height is the shorter side, so we use the width to figure-out what the - // max. height should be given the aspect ratio. - adjustWidth = false; - } - break; - } - if (adjustWidth) { - activityWidth = (int) ((activityHeight / aspectRatioToApply) + 0.5f); - } else { - activityHeight = (int) ((activityWidth / aspectRatioToApply) + 0.5f); - } - } - - if (containingAppWidth <= activityWidth && containingAppHeight <= activityHeight) { - // The display matches or is less than the activity aspect ratio, so nothing else to do. - return false; - } - - // Compute configuration based on max or min supported width and height. - // Also account for the insets (e.g. display cutouts, navigation bar), which will be - // clipped away later in {@link Task#computeConfigResourceOverrides()}, i.e., the out - // bounds are the app bounds restricted by aspect ratio + clippable insets. Otherwise, - // the app bounds would end up too small. To achieve this we will also add clippable insets - // when the corresponding dimension fully fills the parent - - int right = activityWidth + containingAppBounds.left; - int left = containingAppBounds.left; - if (right >= containingAppBounds.right) { - right = containingBounds.right; - left = containingBounds.left; - } - int bottom = activityHeight + containingAppBounds.top; - int top = containingAppBounds.top; - if (bottom >= containingAppBounds.bottom) { - bottom = containingBounds.bottom; - top = containingBounds.top; - } - outBounds.set(left, top, right, bottom); - return true; - } - /** * Returns the min aspect ratio of this activity. */ @@ -9788,10 +9600,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } float getMaxAspectRatio() { - if (mAppCompatController.getTransparentPolicy().isRunning()) { - return mAppCompatController.getTransparentPolicy().getInheritedMaxAspectRatio(); - } - return info.getMaxAspectRatio(); + return mAppCompatController.getAppCompatAspectRatioPolicy().getMaxAspectRatio(); } /** @@ -9802,18 +9611,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } /** - * Returns the aspect ratio of the given {@code rect}. - */ - static float computeAspectRatio(Rect rect) { - final int width = rect.width(); - final int height = rect.height(); - if (width == 0 || height == 0) { - return 0; - } - return Math.max(width, height) / (float) Math.min(width, height); - } - - /** * @return {@code true} if this activity was reparented to another display but * {@link #ensureActivityConfiguration} is not called. */ @@ -9901,8 +9698,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // the combine configurations are equal, but would otherwise differ in the override config mTmpConfig.setTo(mLastReportedConfiguration.getMergedConfiguration()); final ActivityWindowInfo newActivityWindowInfo = getActivityWindowInfo(); - final boolean isActivityWindowInfoChanged = Flags.activityWindowInfoFlag() - && !mLastReportedActivityWindowInfo.equals(newActivityWindowInfo); + final boolean isActivityWindowInfoChanged = + !mLastReportedActivityWindowInfo.equals(newActivityWindowInfo); if (!displayChanged && !isActivityWindowInfoChanged && getConfiguration().equals(mTmpConfig)) { ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration & display " @@ -10039,6 +9836,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ private boolean shouldRelaunchLocked(int changes, Configuration changesConfig) { int configChanged = info.getRealConfigChanged(); + if (android.content.res.Flags.handleAllConfigChanges()) { + if ((configChanged & CONFIG_RESOURCES_UNUSED) != 0) { + // Don't relaunch any activities that claim they do not use resources at all. + // If they still do, the onConfigurationChanged() callback will get called to + // let them know anyway. + return false; + } + } + boolean onlyVrUiModeChanged = onlyVrUiModeChanged(changes, changesConfig); // Override for apps targeting pre-O sdks @@ -10067,8 +9873,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ private boolean onlyVrUiModeChanged(int changes, Configuration lastReportedConfig) { final Configuration currentConfig = getConfiguration(); - return changes == CONFIG_UI_MODE && (isInVrUiMode(currentConfig) - != isInVrUiMode(lastReportedConfig)); + return changes == CONFIG_UI_MODE && (AppCompatUtils.isInVrUiMode(currentConfig) + != AppCompatUtils.isInVrUiMode(lastReportedConfig)); } /** @@ -10440,10 +10246,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return r; } - private static boolean isInVrUiMode(Configuration config) { - return (config.uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_VR_HEADSET; - } - private static boolean isInDeskUiMode(Configuration config) { return (config.uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_DESK; } @@ -10678,7 +10480,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mAppCompatController.getAppCompatAspectRatioOverrides() .shouldEnableUserAspectRatioSettings()); proto.write(IS_USER_FULLSCREEN_OVERRIDE_ENABLED, - mLetterboxUiController.isUserFullscreenOverrideEnabled()); + mAppCompatController.getAppCompatAspectRatioOverrides() + .isUserFullscreenOverrideEnabled()); } @Override diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 5b178750bf55..59b5da8eeb51 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -5183,6 +5183,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { String hostingType) { if (!mStartingProcessActivities.contains(activity)) { mStartingProcessActivities.add(activity); + // Let the activity with higher z-order be started first. + if (mStartingProcessActivities.size() > 1) { + mStartingProcessActivities.sort(null /* by WindowContainer#compareTo */); + } } else if (mProcessNames.get( activity.processName, activity.info.applicationInfo.uid) != null) { // The process is already starting. Wait for it to attach. diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java index 5e6ef4c3bdec..cf008e73321e 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java @@ -32,7 +32,6 @@ import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERR import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE; -import static com.android.server.wm.ActivityRecord.computeAspectRatio; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER; @@ -174,6 +173,11 @@ class AppCompatAspectRatioOverrides { && mActivityRecord.mDisplayContent.getIgnoreOrientationRequest(); } + boolean hasFullscreenOverride() { + // `mUserAspectRatio` is always initialized first in `shouldApplyUserFullscreenOverride()`. + return shouldApplyUserFullscreenOverride() || isSystemOverrideToFullscreenEnabled(); + } + float getUserMinAspectRatio() { switch (mUserAspectRatioState.mUserAspectRatio) { case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE: @@ -211,7 +215,7 @@ class AppCompatAspectRatioOverrides { bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2); bounds.bottom = bounds.centerY(); } - return computeAspectRatio(bounds); + return AppCompatUtils.computeAspectRatio(bounds); } float getFixedOrientationLetterboxAspectRatio(@NonNull Configuration parentConfiguration) { @@ -228,7 +232,7 @@ class AppCompatAspectRatioOverrides { return mActivityRecord.info.getMinAspectRatio(); } final Rect bounds = new Rect(displayArea.getWindowConfiguration().getAppBounds()); - return computeAspectRatio(bounds); + return AppCompatUtils.computeAspectRatio(bounds); } private boolean shouldUseSplitScreenAspectRatio(@NonNull Configuration parentConfiguration) { diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java index db7f745121a3..a7d2ecce4984 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java @@ -23,33 +23,87 @@ import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_PORTRAIT_ONLY; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_SMALL; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_TO_ALIGN_WITH_SPLIT_SCREEN; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static com.android.server.wm.LetterboxConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; +import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; + import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.WindowConfiguration; import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Rect; /** * Encapsulate app compat policy logic related to aspect ratio. */ class AppCompatAspectRatioPolicy { + // Rounding tolerance to be used in aspect ratio computations + private static final float ASPECT_RATIO_ROUNDING_TOLERANCE = 0.005f; + @NonNull private final ActivityRecord mActivityRecord; @NonNull private final TransparentPolicy mTransparentPolicy; @NonNull - private final AppCompatOrientationPolicy mAppCompatOrientationPolicy; - @NonNull private final AppCompatOverrides mAppCompatOverrides; + @NonNull + private final AppCompatAspectRatioState mAppCompatAspectRatioState; AppCompatAspectRatioPolicy(@NonNull ActivityRecord activityRecord, @NonNull TransparentPolicy transparentPolicy, - @NonNull AppCompatOrientationPolicy orientationPolicy, @NonNull AppCompatOverrides appCompatOverrides) { mActivityRecord = activityRecord; mTransparentPolicy = transparentPolicy; - mAppCompatOrientationPolicy = orientationPolicy; mAppCompatOverrides = appCompatOverrides; + mAppCompatAspectRatioState = new AppCompatAspectRatioState(); + } + + /** + * Starts the evaluation of app compat aspect ratio when a new configuration needs to be + * resolved. + */ + void reset() { + mAppCompatAspectRatioState.reset(); + } + + float getDesiredAspectRatio(@NonNull Configuration newParentConfig, + @NonNull Rect parentBounds) { + final float letterboxAspectRatioOverride = + mAppCompatOverrides.getAppCompatAspectRatioOverrides() + .getFixedOrientationLetterboxAspectRatio(newParentConfig); + // Aspect ratio as suggested by the system. Apps requested mix/max aspect ratio will + // be respected in #applyAspectRatio. + if (isDefaultMultiWindowLetterboxAspectRatioDesired(newParentConfig)) { + return DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; + } else if (letterboxAspectRatioOverride > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) { + return letterboxAspectRatioOverride; + } + return AppCompatUtils.computeAspectRatio(parentBounds); + } + + void applyDesiredAspectRatio(@NonNull Configuration newParentConfig, @NonNull Rect parentBounds, + @NonNull Rect resolvedBounds, @NonNull Rect containingBoundsWithInsets, + @NonNull Rect containingBounds) { + final float desiredAspectRatio = getDesiredAspectRatio(newParentConfig, parentBounds); + mAppCompatAspectRatioState.mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, + containingBoundsWithInsets, containingBounds, desiredAspectRatio); + } + + void applyAspectRatioForLetterbox(Rect outBounds, Rect containingAppBounds, + Rect containingBounds) { + mAppCompatAspectRatioState.mIsAspectRatioApplied = applyAspectRatio(outBounds, + containingAppBounds, containingBounds, 0 /* desiredAspectRatio */); + } + + /** + * @return {@code true} when an app compat aspect ratio has been applied. + */ + boolean isAspectRatioApplied() { + return mAppCompatAspectRatioState.mIsAspectRatioApplied; } /** @@ -109,10 +163,217 @@ class AppCompatAspectRatioPolicy { return info.getMinAspectRatio(); } + float getMaxAspectRatio() { + if (mTransparentPolicy.isRunning()) { + return mTransparentPolicy.getInheritedMaxAspectRatio(); + } + return mActivityRecord.info.getMaxAspectRatio(); + } + + @Nullable + Rect getLetterboxedContainerBounds() { + return mAppCompatAspectRatioState.getLetterboxedContainerBounds(); + } + + /** + * Whether this activity is letterboxed for fixed orientation. If letterboxed due to fixed + * orientation then aspect ratio restrictions are also already respected. + * + * <p>This happens when an activity has fixed orientation which doesn't match orientation of the + * parent because a display setting 'ignoreOrientationRequest' is set to true. See {@link + * WindowManagerService#getIgnoreOrientationRequest} for more context. + */ + boolean isLetterboxedForFixedOrientationAndAspectRatio() { + return mAppCompatAspectRatioState.isLetterboxedForFixedOrientationAndAspectRatio(); + } + + boolean isLetterboxedForAspectRatioOnly() { + return mAppCompatAspectRatioState.isLetterboxedForAspectRatioOnly(); + } + + void setLetterboxBoundsForFixedOrientationAndAspectRatio(@NonNull Rect bounds) { + mAppCompatAspectRatioState.mLetterboxBoundsForFixedOrientationAndAspectRatio = bounds; + } + + void setLetterboxBoundsForAspectRatio(@NonNull Rect bounds) { + mAppCompatAspectRatioState.mLetterboxBoundsForAspectRatio = bounds; + } + private boolean isParentFullscreenPortrait() { final WindowContainer<?> parent = mActivityRecord.getParent(); return parent != null && parent.getConfiguration().orientation == ORIENTATION_PORTRAIT && parent.getWindowConfiguration().getWindowingMode() == WINDOWING_MODE_FULLSCREEN; } + + /** + * Applies aspect ratio restrictions to outBounds. If no restrictions, then no change is + * made to outBounds. + * + * @return {@code true} if aspect ratio restrictions were applied. + */ + private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, + Rect containingBounds, float desiredAspectRatio) { + final float maxAspectRatio = getMaxAspectRatio(); + final Task rootTask = mActivityRecord.getRootTask(); + final Task task = mActivityRecord.getTask(); + final float minAspectRatio = getMinAspectRatio(); + final TaskFragment organizedTf = mActivityRecord.getOrganizedTaskFragment(); + float aspectRatioToApply = desiredAspectRatio; + if (task == null || rootTask == null + || (maxAspectRatio < 1 && minAspectRatio < 1 && aspectRatioToApply < 1) + // Don't set aspect ratio if we are in VR mode. + || AppCompatUtils.isInVrUiMode(mActivityRecord.getConfiguration()) + // TODO(b/232898850): Always respect aspect ratio requests. + // Don't set aspect ratio for activity in ActivityEmbedding split. + || (organizedTf != null && !organizedTf.fillsParent())) { + return false; + } + + final int containingAppWidth = containingAppBounds.width(); + final int containingAppHeight = containingAppBounds.height(); + final float containingRatio = AppCompatUtils.computeAspectRatio(containingAppBounds); + + if (aspectRatioToApply < 1) { + aspectRatioToApply = containingRatio; + } + + if (maxAspectRatio >= 1 && aspectRatioToApply > maxAspectRatio) { + aspectRatioToApply = maxAspectRatio; + } else if (minAspectRatio >= 1 && aspectRatioToApply < minAspectRatio) { + aspectRatioToApply = minAspectRatio; + } + + int activityWidth = containingAppWidth; + int activityHeight = containingAppHeight; + + if (containingRatio - aspectRatioToApply > ASPECT_RATIO_ROUNDING_TOLERANCE) { + if (containingAppWidth < containingAppHeight) { + // Width is the shorter side, so we use that to figure-out what the max. height + // should be given the aspect ratio. + activityHeight = (int) ((activityWidth * aspectRatioToApply) + 0.5f); + } else { + // Height is the shorter side, so we use that to figure-out what the max. width + // should be given the aspect ratio. + activityWidth = (int) ((activityHeight * aspectRatioToApply) + 0.5f); + } + } else if (aspectRatioToApply - containingRatio > ASPECT_RATIO_ROUNDING_TOLERANCE) { + boolean adjustWidth; + switch (mActivityRecord.getRequestedConfigurationOrientation()) { + case ORIENTATION_LANDSCAPE: + // Width should be the longer side for this landscape app, so we use the width + // to figure-out what the max. height should be given the aspect ratio. + adjustWidth = false; + break; + case ORIENTATION_PORTRAIT: + // Height should be the longer side for this portrait app, so we use the height + // to figure-out what the max. width should be given the aspect ratio. + adjustWidth = true; + break; + default: + // This app doesn't have a preferred orientation, so we keep the length of the + // longer side, and use it to figure-out the length of the shorter side. + if (containingAppWidth < containingAppHeight) { + // Width is the shorter side, so we use the height to figure-out what the + // max. width should be given the aspect ratio. + adjustWidth = true; + } else { + // Height is the shorter side, so we use the width to figure-out what the + // max. height should be given the aspect ratio. + adjustWidth = false; + } + break; + } + if (adjustWidth) { + activityWidth = (int) ((activityHeight / aspectRatioToApply) + 0.5f); + } else { + activityHeight = (int) ((activityWidth / aspectRatioToApply) + 0.5f); + } + } + + if (containingAppWidth <= activityWidth && containingAppHeight <= activityHeight) { + // The display matches or is less than the activity aspect ratio, so nothing else to do. + return false; + } + + // Compute configuration based on max or min supported width and height. + // Also account for the insets (e.g. display cutouts, navigation bar), which will be + // clipped away later in {@link Task#computeConfigResourceOverrides()}, i.e., the out + // bounds are the app bounds restricted by aspect ratio + clippable insets. Otherwise, + // the app bounds would end up too small. To achieve this we will also add clippable insets + // when the corresponding dimension fully fills the parent + + int right = activityWidth + containingAppBounds.left; + int left = containingAppBounds.left; + if (right >= containingAppBounds.right) { + right = containingBounds.right; + left = containingBounds.left; + } + int bottom = activityHeight + containingAppBounds.top; + int top = containingAppBounds.top; + if (bottom >= containingAppBounds.bottom) { + bottom = containingBounds.bottom; + top = containingBounds.top; + } + outBounds.set(left, top, right, bottom); + return true; + } + + /** + * Returns {@code true} if the default aspect ratio for a letterboxed app in multi-window mode + * should be used. + */ + private boolean isDefaultMultiWindowLetterboxAspectRatioDesired( + @NonNull Configuration parentConfig) { + final DisplayContent dc = mActivityRecord.mDisplayContent; + if (dc == null) { + return false; + } + final int windowingMode = parentConfig.windowConfiguration.getWindowingMode(); + return WindowConfiguration.inMultiWindowMode(windowingMode) + && !dc.getIgnoreOrientationRequest(); + } + + private static class AppCompatAspectRatioState { + // Whether the aspect ratio restrictions applied to the activity bounds + // in applyAspectRatio(). + private boolean mIsAspectRatioApplied = false; + + // Bounds populated in resolveAspectRatioRestriction when this activity is letterboxed for + // aspect ratio. If not null, they are used as parent container in + // resolveSizeCompatModeConfiguration and in a constructor of CompatDisplayInsets. + @Nullable + private Rect mLetterboxBoundsForAspectRatio; + // Bounds populated in resolveFixedOrientationConfiguration when this activity is + // letterboxed for fixed orientation. If not null, they are used as parent container in + // resolveSizeCompatModeConfiguration and in a constructor of CompatDisplayInsets. If + // letterboxed due to fixed orientation then aspect ratio restrictions are also respected. + // This happens when an activity has fixed orientation which doesn't match orientation of + // the parent because a display is ignoring orientation request or fixed to user rotation. + // See WindowManagerService#getIgnoreOrientationRequest and + // WindowManagerService#getFixedToUserRotation for more context. + @Nullable + private Rect mLetterboxBoundsForFixedOrientationAndAspectRatio; + + @Nullable + Rect getLetterboxedContainerBounds() { + return mLetterboxBoundsForFixedOrientationAndAspectRatio != null + ? mLetterboxBoundsForFixedOrientationAndAspectRatio + : mLetterboxBoundsForAspectRatio; + } + + void reset() { + mIsAspectRatioApplied = false; + mLetterboxBoundsForFixedOrientationAndAspectRatio = null; + mLetterboxBoundsForAspectRatio = null; + } + + boolean isLetterboxedForFixedOrientationAndAspectRatio() { + return mLetterboxBoundsForFixedOrientationAndAspectRatio != null; + } + + boolean isLetterboxedForAspectRatioOnly() { + return mLetterboxBoundsForAspectRatio != null; + } + } } diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index acd1d5ab7e9b..3eed96dd90c2 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -49,7 +49,7 @@ class AppCompatController { wmService.mLetterboxConfiguration, optPropBuilder); mOrientationPolicy = new AppCompatOrientationPolicy(activityRecord, mAppCompatOverrides); mAppCompatAspectRatioPolicy = new AppCompatAspectRatioPolicy(activityRecord, - mTransparentPolicy, mOrientationPolicy, mAppCompatOverrides); + mTransparentPolicy, mAppCompatOverrides); } @NonNull diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java index 155e246db7de..9bf80119e431 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java @@ -50,7 +50,6 @@ class AppCompatOrientationOverrides { private final ActivityRecord mActivityRecord; @NonNull private final AppCompatCameraOverrides mAppCompatCameraOverrides; - @NonNull private final OptPropFactory.OptProp mIgnoreRequestedOrientationOptProp; @NonNull @@ -109,7 +108,8 @@ class AppCompatOrientationOverrides { mOrientationOverridesState.updateOrientationRequestLoopState(); return mOrientationOverridesState.shouldIgnoreRequestInLoop() - && !mActivityRecord.isLetterboxedForFixedOrientationAndAspectRatio(); + && !mActivityRecord.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio(); } /** diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index be51dd3bcf1c..1b30a20d3e9a 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -16,7 +16,12 @@ package com.android.server.wm; +import static android.content.res.Configuration.UI_MODE_TYPE_MASK; +import static android.content.res.Configuration.UI_MODE_TYPE_VR_HEADSET; + import android.annotation.NonNull; +import android.content.res.Configuration; +import android.graphics.Rect; import java.util.function.BooleanSupplier; @@ -48,4 +53,24 @@ class AppCompatUtils { } }; } + + /** + * Returns the aspect ratio of the given {@code rect}. + */ + static float computeAspectRatio(Rect rect) { + final int width = rect.width(); + final int height = rect.height(); + if (width == 0 || height == 0) { + return 0; + } + return Math.max(width, height) / (float) Math.min(width, height); + } + + /** + * @param config The current {@link Configuration} + * @return {@code true} if using a VR headset. + */ + static boolean isInVrUiMode(Configuration config) { + return (config.uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_VR_HEADSET; + } } diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 44b414f1ea7e..78636a7870cf 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -98,7 +98,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.function.Consumer; import java.util.function.Predicate; /** @@ -290,12 +289,10 @@ public class AppTransitionController { getTopApp(mDisplayContent.mChangingContainers, false /* ignoreHidden */); final WindowManager.LayoutParams animLp = getAnimLp(animLpActivity); - // Check if there is any override - if (!overrideWithTaskFragmentRemoteAnimation(transit, activityTypes)) { - // Unfreeze the windows that were previously frozen for TaskFragment animation. - unfreezeEmbeddedChangingWindows(); - overrideWithRemoteAnimationIfSet(animLpActivity, transit, activityTypes); - } + // No AE remote animation with Shell transition. + // Unfreeze the windows that were previously frozen for TaskFragment animation. + unfreezeEmbeddedChangingWindows(); + overrideWithRemoteAnimationIfSet(animLpActivity, transit, activityTypes); final boolean voiceInteraction = containsVoiceInteraction(mDisplayContent.mClosingApps) || containsVoiceInteraction(mDisplayContent.mOpeningApps); @@ -726,64 +723,6 @@ public class AppTransitionController { } /** - * Overrides the pending transition with the remote animation defined by the - * {@link ITaskFragmentOrganizer} if all windows in the transition are children of - * {@link TaskFragment} that are organized by the same organizer. - * - * @return {@code true} if the transition is overridden. - */ - private boolean overrideWithTaskFragmentRemoteAnimation(@TransitionOldType int transit, - ArraySet<Integer> activityTypes) { - if (transitionMayContainNonAppWindows(transit)) { - return false; - } - if (!transitionContainsTaskFragmentWithBoundsOverride()) { - // No need to play TaskFragment remote animation if all embedded TaskFragment in the - // transition fill the Task. - return false; - } - - final Task task = findParentTaskForAllEmbeddedWindows(); - final ITaskFragmentOrganizer organizer = findTaskFragmentOrganizer(task); - final RemoteAnimationDefinition definition = organizer != null - ? mDisplayContent.mAtmService.mTaskFragmentOrganizerController - .getRemoteAnimationDefinition(organizer) - : null; - final RemoteAnimationAdapter adapter = definition != null - ? definition.getAdapter(transit, activityTypes) - : null; - if (adapter == null) { - return false; - } - mDisplayContent.mAppTransition.overridePendingAppTransitionRemote( - adapter, false /* sync */, true /*isActivityEmbedding*/); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, - "Override with TaskFragment remote animation for transit=%s", - AppTransition.appTransitionOldToString(transit)); - - final int organizerUid = mDisplayContent.mAtmService.mTaskFragmentOrganizerController - .getTaskFragmentOrganizerUid(organizer); - final boolean shouldDisableInputForRemoteAnimation = !task.isFullyTrustedEmbedding( - organizerUid); - final RemoteAnimationController remoteAnimationController = - mDisplayContent.mAppTransition.getRemoteAnimationController(); - if (shouldDisableInputForRemoteAnimation && remoteAnimationController != null) { - // We are going to use client-driven animation, Disable all input on activity windows - // during the animation (unless it is fully trusted) to ensure it is safe to allow - // client to animate the surfaces. - // This is needed for all activity windows in the animation Task. - remoteAnimationController.setOnRemoteAnimationReady(() -> { - final Consumer<ActivityRecord> updateActivities = - activity -> activity.setDropInputForAnimation(true); - task.forAllActivities(updateActivities); - }); - ProtoLog.d(WM_DEBUG_APP_TRANSITIONS, "Task=%d contains embedded TaskFragment." - + " Disabled all input during TaskFragment remote animation.", task.mTaskId); - } - return true; - } - - /** * Overrides the pending transition with the remote animation defined for the transition in the * set of defined remote animations in the app window token. */ diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index a4fb95964a5c..54024e92f95f 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -128,21 +128,24 @@ public class BackgroundActivityStartController { // TODO(b/263368846) Rename when ASM logic is moved in @Retention(SOURCE) - @IntDef({BAL_BLOCK, - BAL_ALLOW_DEFAULT, - BAL_ALLOW_ALLOWLISTED_UID, + @IntDef({ BAL_ALLOW_ALLOWLISTED_COMPONENT, - BAL_ALLOW_VISIBLE_WINDOW, + BAL_ALLOW_ALLOWLISTED_UID, + BAL_ALLOW_BOUND_BY_FOREGROUND, + BAL_ALLOW_DEFAULT, + BAL_ALLOW_FOREGROUND, + BAL_ALLOW_GRACE_PERIOD, BAL_ALLOW_PENDING_INTENT, BAL_ALLOW_PERMISSION, BAL_ALLOW_SAW_PERMISSION, - BAL_ALLOW_GRACE_PERIOD, - BAL_ALLOW_FOREGROUND, - BAL_ALLOW_SDK_SANDBOX + BAL_ALLOW_SDK_SANDBOX, + BAL_ALLOW_TOKEN, + BAL_ALLOW_VISIBLE_WINDOW, + BAL_BLOCK }) public @interface BalCode {} - static final int BAL_BLOCK = 0; + static final int BAL_BLOCK = FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_BLOCKED; static final int BAL_ALLOW_DEFAULT = FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_DEFAULT; @@ -195,10 +198,19 @@ public class BackgroundActivityStartController { static final int BAL_ALLOW_NON_APP_VISIBLE_WINDOW = FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_NON_APP_VISIBLE_WINDOW; + /** Process belongs to a SDK sandbox */ + static final int BAL_ALLOW_TOKEN = + FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_TOKEN; + + /** Process belongs to a SDK sandbox */ + static final int BAL_ALLOW_BOUND_BY_FOREGROUND = + FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_BOUND_BY_FOREGROUND; + static String balCodeToString(@BalCode int balCode) { return switch (balCode) { case BAL_ALLOW_ALLOWLISTED_COMPONENT -> "BAL_ALLOW_ALLOWLISTED_COMPONENT"; case BAL_ALLOW_ALLOWLISTED_UID -> "BAL_ALLOW_ALLOWLISTED_UID"; + case BAL_ALLOW_BOUND_BY_FOREGROUND -> "BAL_ALLOW_BOUND_BY_FOREGROUND"; case BAL_ALLOW_DEFAULT -> "BAL_ALLOW_DEFAULT"; case BAL_ALLOW_FOREGROUND -> "BAL_ALLOW_FOREGROUND"; case BAL_ALLOW_GRACE_PERIOD -> "BAL_ALLOW_GRACE_PERIOD"; @@ -207,6 +219,7 @@ public class BackgroundActivityStartController { case BAL_ALLOW_PERMISSION -> "BAL_ALLOW_PERMISSION"; case BAL_ALLOW_SAW_PERMISSION -> "BAL_ALLOW_SAW_PERMISSION"; case BAL_ALLOW_SDK_SANDBOX -> "BAL_ALLOW_SDK_SANDBOX"; + case BAL_ALLOW_TOKEN -> "BAL_ALLOW_TOKEN"; case BAL_ALLOW_VISIBLE_WINDOW -> "BAL_ALLOW_VISIBLE_WINDOW"; case BAL_BLOCK -> "BAL_BLOCK"; default -> throw new IllegalArgumentException("Unexpected value: " + balCode); @@ -1042,7 +1055,9 @@ public class BackgroundActivityStartController { || balCode == BAL_ALLOW_PENDING_INTENT || balCode == BAL_ALLOW_SAW_PERMISSION || balCode == BAL_ALLOW_VISIBLE_WINDOW - || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW) { + || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW + || balCode == BAL_ALLOW_TOKEN + || balCode == BAL_ALLOW_BOUND_BY_FOREGROUND) { return true; } } @@ -1266,7 +1281,8 @@ public class BackgroundActivityStartController { || balCode == BAL_ALLOW_PERMISSION || balCode == BAL_ALLOW_SAW_PERMISSION || balCode == BAL_ALLOW_VISIBLE_WINDOW - || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW) { + || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW + || balCode == BAL_ALLOW_BOUND_BY_FOREGROUND) { return; } @@ -1572,7 +1588,7 @@ public class BackgroundActivityStartController { } if (balCode == BAL_ALLOW_VISIBLE_WINDOW || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW - || balCode == BAL_ALLOW_FOREGROUND) { + || balCode == BAL_ALLOW_FOREGROUND || balCode == BAL_ALLOW_BOUND_BY_FOREGROUND) { Task task = sourceRecord != null ? sourceRecord.getTask() : targetTask; if (task != null && task.getDisplayArea() != null) { joiner.add(prefix + "Tasks: "); @@ -1659,6 +1675,8 @@ public class BackgroundActivityStartController { activityName = ""; } writeBalAllowedLog(activityName, finalVerdict.getCode(), state); + } else { + writeBalAllowedLogMinimal(state); } } else { @BalCode int code = finalVerdict.getCode(); @@ -1723,6 +1741,24 @@ public class BackgroundActivityStartController { ); } + @VisibleForTesting void writeBalAllowedLogMinimal(BalState state) { + FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, + "", + BAL_ALLOW_DEFAULT, + NO_PROCESS_UID, + NO_PROCESS_UID, + state.mResultForCaller == null ? BAL_BLOCK : state.mResultForCaller.getRawCode(), + state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts(), + state.callerExplicitOptInOrOut(), + state.mResultForRealCaller == null ? BAL_BLOCK + : state.mResultForRealCaller.getRawCode(), + state.mBalAllowedByPiSender.allowsBackgroundActivityStarts(), + state.realCallerExplicitOptInOrOut(), + getTargetSdk(state.mCallingPackage), + getTargetSdk(state.mRealCallingPackage) + ); + } + /** * Called whenever an activity finishes. Stores the record, so it can be used by ASM grace * period checks. diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java index 478524b7bd1c..4a870a3a5b6e 100644 --- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java +++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java @@ -23,10 +23,13 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLAS import static com.android.server.wm.ActivityTaskManagerService.ACTIVITY_BG_START_GRACE_PERIOD_MS; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_DISALLOW; +import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_BOUND_BY_FOREGROUND; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_FOREGROUND; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_GRACE_PERIOD; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION; +import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_TOKEN; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW; +import static com.android.window.flags.Flags.balImprovedMetrics; import static java.util.Objects.requireNonNull; @@ -110,8 +113,8 @@ class BackgroundLaunchProcessController { } // Allow if the flag was explicitly set. if (isBackgroundStartAllowedByToken(uid, packageName, isCheckingForFgsStart)) { - return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true, - "process allowed by token"); + return new BalVerdict(balImprovedMetrics() ? BAL_ALLOW_TOKEN : BAL_ALLOW_PERMISSION, + /*background*/ true, "process allowed by token"); } // Allow if the caller is bound by a UID that's currently foreground. // But still respect the appSwitchState. @@ -120,7 +123,8 @@ class BackgroundLaunchProcessController { ? appSwitchState != APP_SWITCH_DISALLOW && isBoundByForegroundUid() : isBoundByForegroundUid(); if (allowBoundByForegroundUid) { - return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false, + return new BalVerdict(balImprovedMetrics() ? BAL_ALLOW_BOUND_BY_FOREGROUND + : BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false, "process bound by foreground uid"); } // Allow if the caller has an activity in any foreground task. diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 3925de8162f8..235d6cd09d35 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -472,51 +472,11 @@ final class LetterboxUiController { return !isHorizontalThinLetterboxed(); } - /** - * Whether we should apply the user aspect ratio override to the min aspect ratio for the - * current app. - */ - boolean shouldApplyUserMinAspectRatioOverride() { - return mActivityRecord.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldApplyUserMinAspectRatioOverride(); - } - - boolean shouldApplyUserFullscreenOverride() { - return mActivityRecord.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldApplyUserFullscreenOverride(); - } - - boolean isUserFullscreenOverrideEnabled() { - return mActivityRecord.mAppCompatController.getAppCompatAspectRatioOverrides() - .isUserFullscreenOverrideEnabled(); - } - - boolean isSystemOverrideToFullscreenEnabled() { - return mActivityRecord.mAppCompatController.getAppCompatAspectRatioOverrides() - .isSystemOverrideToFullscreenEnabled(); - } - - boolean hasFullscreenOverride() { - // `mUserAspectRatio` is always initialized first in `shouldApplyUserFullscreenOverride()`. - return shouldApplyUserFullscreenOverride() || isSystemOverrideToFullscreenEnabled(); - } - - float getUserMinAspectRatio() { - return mActivityRecord.mAppCompatController.getAppCompatAspectRatioOverrides() - .getUserMinAspectRatio(); - } - boolean shouldOverrideMinAspectRatio() { return mActivityRecord.mAppCompatController.getAppCompatAspectRatioOverrides() .shouldOverrideMinAspectRatio(); } - @VisibleForTesting - int getUserMinAspectRatioOverrideCode() { - return mActivityRecord.mAppCompatController.getAppCompatAspectRatioOverrides() - .getUserMinAspectRatioOverrideCode(); - } - @LetterboxConfiguration.LetterboxVerticalReachabilityPosition int getLetterboxPositionForVerticalReachability() { final boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge(); @@ -937,7 +897,7 @@ final class LetterboxUiController { pw.println(prefix + " letterboxReason=" + getLetterboxReasonString(mainWin)); pw.println(prefix + " activityAspectRatio=" - + mActivityRecord.computeAspectRatio(mActivityRecord.getBounds())); + + AppCompatUtils.computeAspectRatio(mActivityRecord.getBounds())); boolean shouldShowLetterboxUi = shouldShowLetterboxUi(mainWin); pw.println(prefix + "shouldShowLetterboxUi=" + shouldShowLetterboxUi); @@ -996,13 +956,15 @@ final class LetterboxUiController { if (mActivityRecord.inSizeCompatMode()) { return "SIZE_COMPAT_MODE"; } - if (mActivityRecord.isLetterboxedForFixedOrientationAndAspectRatio()) { + if (mActivityRecord.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()) { return "FIXED_ORIENTATION"; } if (mainWin.isLetterboxedForDisplayCutout()) { return "DISPLAY_CUTOUT"; } - if (mActivityRecord.isLetterboxedForAspectRatioOnly()) { + if (mActivityRecord.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForAspectRatioOnly()) { return "ASPECT_RATIO"; } return "UNKNOWN_REASON"; diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java index a8edaebd24a6..f8665c70c61d 100644 --- a/services/core/java/com/android/server/wm/RemoteAnimationController.java +++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java @@ -44,8 +44,8 @@ import android.view.SurfaceControl.Transaction; import android.view.WindowManager; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.LogLevel; import com.android.internal.util.FastPrintWriter; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; @@ -53,7 +53,6 @@ import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; -import java.util.function.Consumer; /** * Helper class to run app animations in a remote process. @@ -349,10 +348,6 @@ class RemoteAnimationController implements DeathRecipient { } finally { mIsFinishing = false; } - // Reset input for all activities when the remote animation is finished. - final Consumer<ActivityRecord> updateActivities = - activity -> activity.setDropInputForAnimation(false); - mDisplayContent.forAllActivities(updateActivities); } setRunningRemoteAnimation(false); ProtoLog.i(WM_DEBUG_REMOTE_ANIMATIONS, "Finishing remote animation"); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 1d661920557d..74f8a0683811 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3406,9 +3406,11 @@ class Task extends TaskFragment { appCompatTaskInfo.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET; appCompatTaskInfo.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET; appCompatTaskInfo.isUserFullscreenOverrideEnabled = top != null - && top.mLetterboxUiController.shouldApplyUserFullscreenOverride(); + && top.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldApplyUserFullscreenOverride(); appCompatTaskInfo.isSystemFullscreenOverrideEnabled = top != null - && top.mLetterboxUiController.isSystemOverrideToFullscreenEnabled(); + && top.mAppCompatController.getAppCompatAspectRatioOverrides() + .isSystemOverrideToFullscreenEnabled(); appCompatTaskInfo.isFromLetterboxDoubleTap = top != null && top.mLetterboxUiController.isFromDoubleTap(); if (top != null) { diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index b6b6cf2dc430..439c7bb4050b 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -46,7 +46,6 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; -import android.view.RemoteAnimationDefinition; import android.view.WindowManager; import android.window.ITaskFragmentOrganizer; import android.window.ITaskFragmentOrganizerController; @@ -58,8 +57,8 @@ import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.ProtoLogGroup; import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.ProtoLogGroup; import com.android.window.flags.Flags; import java.lang.annotation.Retention; @@ -146,13 +145,6 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr private final boolean mIsSystemOrganizer; /** - * {@link RemoteAnimationDefinition} for embedded activities transition animation that is - * organized by this organizer. - */ - @Nullable - private RemoteAnimationDefinition mRemoteAnimationDefinition; - - /** * Map from {@link TaskFragmentTransaction#getTransactionToken()} to the * {@link Transition#getSyncId()} that has been deferred. {@link TransitionController} will * wait until the organizer finished handling the {@link TaskFragmentTransaction}. @@ -533,50 +525,6 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } @Override - public void registerRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer, - @NonNull RemoteAnimationDefinition definition) { - final int pid = Binder.getCallingPid(); - final int uid = Binder.getCallingUid(); - synchronized (mGlobalLock) { - ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, - "Register remote animations for organizer=%s uid=%d pid=%d", - organizer.asBinder(), uid, pid); - final TaskFragmentOrganizerState organizerState = - mTaskFragmentOrganizerState.get(organizer.asBinder()); - if (organizerState == null) { - throw new IllegalStateException("The organizer hasn't been registered."); - } - if (organizerState.mRemoteAnimationDefinition != null) { - throw new IllegalStateException( - "The organizer has already registered remote animations=" - + organizerState.mRemoteAnimationDefinition); - } - - definition.setCallingPidUid(pid, uid); - organizerState.mRemoteAnimationDefinition = definition; - } - } - - @Override - public void unregisterRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer) { - final int pid = Binder.getCallingPid(); - final long uid = Binder.getCallingUid(); - synchronized (mGlobalLock) { - ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, - "Unregister remote animations for organizer=%s uid=%d pid=%d", - organizer.asBinder(), uid, pid); - final TaskFragmentOrganizerState organizerState = - mTaskFragmentOrganizerState.get(organizer.asBinder()); - if (organizerState == null) { - Slog.e(TAG, "The organizer hasn't been registered."); - return; - } - - organizerState.mRemoteAnimationDefinition = null; - } - } - - @Override public void onTransactionHandled(@NonNull IBinder transactionToken, @NonNull WindowContainerTransaction wct, @WindowManager.TransitionType int transitionType, boolean shouldApplyIndependently) { @@ -617,25 +565,6 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } } - /** - * Gets the {@link RemoteAnimationDefinition} set on the given organizer if exists. Returns - * {@code null} if it doesn't. - */ - @Nullable - public RemoteAnimationDefinition getRemoteAnimationDefinition( - @NonNull ITaskFragmentOrganizer organizer) { - synchronized (mGlobalLock) { - final TaskFragmentOrganizerState organizerState = - mTaskFragmentOrganizerState.get(organizer.asBinder()); - if (organizerState == null) { - Slog.e(TAG, "TaskFragmentOrganizer has been unregistered or died when trying" - + " to play animation on its organized windows."); - return null; - } - return organizerState.mRemoteAnimationDefinition; - } - } - int getTaskFragmentOrganizerUid(@NonNull ITaskFragmentOrganizer organizer) { final TaskFragmentOrganizerState state = validateAndGetState(organizer); return state.mOrganizerUid; diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 47af6fc0a6c7..2a3e94544aea 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2757,12 +2757,19 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return out; } + // Get the animation theme from the top-most application window + // when Flags.customAnimationsBehindTranslucent() is false. final AnimationOptions animOptionsForActivityTransition = calculateAnimationOptionsForActivityTransition(type, sortedTargets); + if (!Flags.moveAnimationOptionsToChange() && animOptionsForActivityTransition != null) { out.setAnimationOptions(animOptionsForActivityTransition); } + // Store the animation options of the topmost non-translucent change + // (Used when Flags.customAnimationsBehindTranslucent() is true) + AnimationOptions activityAboveAnimationOptions = null; + final ArraySet<WindowContainer> occludedAtEndContainers = new ArraySet<>(); // Convert all the resolved ChangeInfos into TransactionInfo.Change objects in order. final int count = sortedTargets.size(); @@ -2881,9 +2888,26 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { change.setBackgroundColor(ColorUtils.setAlphaComponent(backgroundColor, 255)); } - AnimationOptions animOptions = null; + // Calculate the animation options for this change if (Flags.moveAnimationOptionsToChange()) { - if (activityRecord != null && animOptionsForActivityTransition != null) { + AnimationOptions animOptions = null; + if (Flags.customAnimationsBehindTranslucent() && activityRecord != null) { + if (activityAboveAnimationOptions != null) { + // Inherit the options from one of the changes on top of this + animOptions = activityAboveAnimationOptions; + } else { + // Create the options based on this change's custom animations and layout + // parameters + animOptions = getOptions(activityRecord /* customAnimActivity */, + activityRecord /* animLpActivity */); + if (!change.hasFlags(FLAG_TRANSLUCENT)) { + // If this change is not translucent, its options are going to be + // inherited by the changes below + activityAboveAnimationOptions = animOptions; + } + } + } else if (activityRecord != null && animOptionsForActivityTransition != null) { + // Use the same options from the top activity for all the activities animOptions = animOptionsForActivityTransition; } else if (Flags.activityEmbeddingOverlayPresentationFlag() && isEmbeddedTaskFragment) { @@ -2931,25 +2955,42 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { @Nullable private static AnimationOptions calculateAnimationOptionsForActivityTransition( @TransitionType int type, @NonNull ArrayList<ChangeInfo> sortedTargets) { - TransitionInfo.AnimationOptions animOptions = null; - - // Check if the top-most app is an activity (ie. activity->activity). If so, make sure - // to honor its custom transition options. WindowContainer<?> topApp = null; for (int i = 0; i < sortedTargets.size(); i++) { - if (isWallpaper(sortedTargets.get(i).mContainer)) continue; - topApp = sortedTargets.get(i).mContainer; - break; + if (!isWallpaper(sortedTargets.get(i).mContainer)) { + topApp = sortedTargets.get(i).mContainer; + break; + } } - if (topApp.asActivityRecord() != null) { - final ActivityRecord topActivity = topApp.asActivityRecord(); - animOptions = addCustomActivityTransition(topActivity, true/* open */, - null /* animOptions */); - animOptions = addCustomActivityTransition(topActivity, false/* open */, + ActivityRecord animLpActivity = findAnimLayoutParamsActivityRecord(type, sortedTargets); + return getOptions(topApp.asActivityRecord() /* customAnimActivity */, + animLpActivity /* animLpActivity */); + } + + /** + * Updates and returns animOptions with the layout parameters of animLpActivity + * @param customAnimActivity the activity that drives the custom animation options + * @param animLpActivity the activity that drives the animation options with its layout + * parameters + * @return the options extracted from the provided activities + */ + @Nullable + private static AnimationOptions getOptions(@Nullable ActivityRecord customAnimActivity, + @Nullable ActivityRecord animLpActivity) { + AnimationOptions animOptions = null; + // Custom + if (customAnimActivity != null) { + animOptions = addCustomActivityTransition(customAnimActivity, true /* open */, + animOptions); + animOptions = addCustomActivityTransition(customAnimActivity, false /* open */, animOptions); } - final WindowManager.LayoutParams animLp = - getLayoutParamsForAnimationsStyle(type, sortedTargets); + + // Layout parameters + final WindowState mainWindow = animLpActivity != null + ? animLpActivity.findMainWindow() : null; + final WindowManager.LayoutParams animLp = mainWindow != null ? mainWindow.mAttrs : null; + if (animLp != null && animLp.type != TYPE_APPLICATION_STARTING && animLp.windowAnimations != 0) { // Don't send animation options if no windowAnimations have been set or if the we @@ -3087,10 +3128,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return ancestor; } - private static WindowManager.LayoutParams getLayoutParamsForAnimationsStyle(int type, - ArrayList<ChangeInfo> sortedTargets) { - // Find the layout params of the top-most application window that is part of the - // transition, which is what will control the animation theme. + @Nullable + private static ActivityRecord findAnimLayoutParamsActivityRecord( + @TransitionType int transit, @NonNull List<ChangeInfo> sortedTargets) { final ArraySet<Integer> activityTypes = new ArraySet<>(); final int targetCount = sortedTargets.size(); for (int i = 0; i < targetCount; ++i) { @@ -3110,16 +3150,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // activity through the layout parameter animation style. return null; } - final ActivityRecord animLpActivity = - findAnimLayoutParamsActivityRecord(sortedTargets, type, activityTypes); - final WindowState mainWindow = animLpActivity != null - ? animLpActivity.findMainWindow() : null; - return mainWindow != null ? mainWindow.mAttrs : null; - } - private static ActivityRecord findAnimLayoutParamsActivityRecord( - List<ChangeInfo> sortedTargets, - @TransitionType int transit, ArraySet<Integer> activityTypes) { // Remote animations always win, but fullscreen windows override non-fullscreen windows. ActivityRecord result = lookForTopWindowWithFilter(sortedTargets, w -> w.getRemoteAnimationDefinition() != null diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 1d02f1c133f6..eb1a80b74b76 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -117,6 +117,7 @@ import com.android.server.wm.SurfaceAnimator.Animatable; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; import com.android.server.wm.utils.AlwaysTruePredicate; +import com.android.window.flags.Flags; import java.io.PrintWriter; import java.lang.ref.WeakReference; @@ -458,6 +459,9 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< source.setFrame(provider.getArbitraryRectangle()) .updateSideHint(getBounds()) .setBoundingRects(provider.getBoundingRects()); + if (Flags.enableCaptionCompatInsetForceConsumption()) { + source.setFlags(provider.getFlags()); + } mLocalInsetsSources.put(id, source); mDisplayContent.getInsetsStateController().updateAboveInsetsState(true); } @@ -2579,8 +2583,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // Containers don't belong to the same hierarchy??? if (commonAncestor == null) { - throw new IllegalArgumentException("No in the same hierarchy this=" - + thisParentChain + " other=" + otherParentChain); + final int thisZ = getPrefixOrderIndex(); + final int otherZ = other.getPrefixOrderIndex(); + Slog.w(TAG, "Compare not in the same hierarchy this=" + + thisParentChain + " thisZ=" + thisZ + " other=" + + otherParentChain + " otherZ=" + otherZ); + return Integer.compare(thisZ, otherZ); } // Children are always considered greater than their parents, so if one of the containers diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index de73e6cfad12..228eb7626e7c 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1101,7 +1101,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mPolicy = mWmService.mPolicy; mContext = mWmService.mContext; mForceSeamlesslyRotate = token.mRoundedCornerOverlay; - mLastReportedActivityWindowInfo = Flags.activityWindowInfoFlag() && mActivityRecord != null + mLastReportedActivityWindowInfo = mActivityRecord != null ? new ActivityWindowInfo() : null; mInputWindowHandle = new InputWindowHandleWrapper(new InputWindowHandle( diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java index ba5323ee1f8f..21f7eca5627a 100644 --- a/services/core/java/com/android/server/wm/WindowTracing.java +++ b/services/core/java/com/android/server/wm/WindowTracing.java @@ -18,89 +18,52 @@ package com.android.server.wm; import static android.os.Build.IS_USER; -import static com.android.server.wm.WindowManagerTraceFileProto.ENTRY; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L; -import static com.android.server.wm.WindowManagerTraceFileProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS; import static com.android.server.wm.WindowManagerTraceProto.ELAPSED_REALTIME_NANOS; import static com.android.server.wm.WindowManagerTraceProto.WHERE; import static com.android.server.wm.WindowManagerTraceProto.WINDOW_MANAGER_SERVICE; import android.annotation.Nullable; import android.os.ShellCommand; -import android.os.SystemClock; import android.os.Trace; import android.util.Log; import android.util.proto.ProtoOutputStream; import android.view.Choreographer; import com.android.internal.protolog.LegacyProtoLogImpl; -import com.android.internal.protolog.common.IProtoLog; import com.android.internal.protolog.ProtoLog; -import com.android.internal.util.TraceBuffer; -import java.io.File; -import java.io.IOException; import java.io.PrintWriter; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; /** - * A class that allows window manager to dump its state continuously to a trace file, such that a + * A class that allows window manager to dump its state continuously, such that a * time series of window manager state can be analyzed after the fact. */ -class WindowTracing { - - /** - * Maximum buffer size, currently defined as 5 MB - * Size was experimentally defined to fit between 100 to 150 elements. - */ - private static final int BUFFER_CAPACITY_CRITICAL = 5120 * 1024; // 5 MB - private static final int BUFFER_CAPACITY_TRIM = 10240 * 1024; // 10 MB - private static final int BUFFER_CAPACITY_ALL = 20480 * 1024; // 20 MB - static final String WINSCOPE_EXT = ".winscope"; - private static final String TRACE_FILENAME = "/data/misc/wmtrace/wm_trace" + WINSCOPE_EXT; - private static final String TAG = "WindowTracing"; - private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; +abstract class WindowTracing { + protected static final String TAG = "WindowTracing"; + protected static final String WHERE_START_TRACING = "trace.enable"; + protected static final String WHERE_ON_FRAME = "onFrame"; private final WindowManagerService mService; private final Choreographer mChoreographer; private final WindowManagerGlobalLock mGlobalLock; - private final Object mEnabledLock = new Object(); - private final File mTraceFile; - private final TraceBuffer mBuffer; private final Choreographer.FrameCallback mFrameCallback = (frameTimeNanos) -> - log("onFrame" /* where */); + log(WHERE_ON_FRAME); - private @WindowTraceLogLevel int mLogLevel = WindowTraceLogLevel.TRIM; - private boolean mLogOnFrame = false; - private boolean mEnabled; - private volatile boolean mEnabledLockFree; - private boolean mScheduled; + private AtomicBoolean mScheduled = new AtomicBoolean(false); - private final IProtoLog mProtoLog; static WindowTracing createDefaultAndStartLooper(WindowManagerService service, Choreographer choreographer) { - File file = new File(TRACE_FILENAME); - return new WindowTracing(file, service, choreographer, BUFFER_CAPACITY_TRIM); + return new WindowTracingLegacy(service, choreographer); } - private WindowTracing(File file, WindowManagerService service, Choreographer choreographer, - int bufferCapacity) { - this(file, service, choreographer, service.mGlobalLock, bufferCapacity); - } - - WindowTracing(File file, WindowManagerService service, Choreographer choreographer, - WindowManagerGlobalLock globalLock, int bufferCapacity) { + protected WindowTracing(WindowManagerService service, Choreographer choreographer, + WindowManagerGlobalLock globalLock) { mChoreographer = choreographer; mService = service; mGlobalLock = globalLock; - mTraceFile = file; - mBuffer = new TraceBuffer(bufferCapacity); - setLogLevel(WindowTraceLogLevel.TRIM, null /* pw */); - mProtoLog = ProtoLog.getSingleInstance(); } void startTrace(@Nullable PrintWriter pw) { @@ -108,44 +71,29 @@ class WindowTracing { logAndPrintln(pw, "Error: Tracing is not supported on user builds."); return; } - synchronized (mEnabledLock) { - if (!android.tracing.Flags.perfettoProtologTracing()) { - ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw); - } - logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); - mBuffer.resetBuffer(); - mEnabled = mEnabledLockFree = true; + if (!android.tracing.Flags.perfettoProtologTracing()) { + ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw); } - log("trace.enable"); + startTraceInternal(pw); } - /** - * Stops the trace and write the current buffer to disk - * @param pw Print writer - */ void stopTrace(@Nullable PrintWriter pw) { if (IS_USER) { logAndPrintln(pw, "Error: Tracing is not supported on user builds."); return; } - synchronized (mEnabledLock) { - logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush."); - mEnabled = mEnabledLockFree = false; - - if (mEnabled) { - logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush."); - throw new IllegalStateException("tracing enabled while waiting for flush."); - } - writeTraceToFileLocked(); - logAndPrintln(pw, "Trace written to " + mTraceFile + "."); - } if (!android.tracing.Flags.perfettoProtologTracing()) { ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).stopProtoLog(pw, true); } + stopTraceInternal(pw); } /** - * Stops the trace and write the current buffer to disk then restart, if it's already running. + * If legacy tracing is enabled (either WM or ProtoLog): + * 1. Stop tracing + * 2. Write trace to disk (to be picked by dumpstate) + * 3. Restart tracing + * * @param pw Print writer */ void saveForBugreport(@Nullable PrintWriter pw) { @@ -153,143 +101,24 @@ class WindowTracing { logAndPrintln(pw, "Error: Tracing is not supported on user builds."); return; } - synchronized (mEnabledLock) { - if (!mEnabled) { - return; - } - mEnabled = mEnabledLockFree = false; - logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush."); - writeTraceToFileLocked(); - logAndPrintln(pw, "Trace written to " + mTraceFile + "."); - if (!android.tracing.Flags.perfettoProtologTracing()) { - ((LegacyProtoLogImpl) mProtoLog).stopProtoLog(pw, true); - } - logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); - mBuffer.resetBuffer(); - mEnabled = mEnabledLockFree = true; - if (!android.tracing.Flags.perfettoProtologTracing()) { - ((LegacyProtoLogImpl) mProtoLog).startProtoLog(pw); - } - } - } - - private void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw) { - logAndPrintln(pw, "Setting window tracing log level to " + logLevel); - mLogLevel = logLevel; - - switch (logLevel) { - case WindowTraceLogLevel.ALL: { - setBufferCapacity(BUFFER_CAPACITY_ALL, pw); - break; - } - case WindowTraceLogLevel.TRIM: { - setBufferCapacity(BUFFER_CAPACITY_TRIM, pw); - break; - } - case WindowTraceLogLevel.CRITICAL: { - setBufferCapacity(BUFFER_CAPACITY_CRITICAL, pw); - break; - } - } - } - - private void setLogFrequency(boolean onFrame, PrintWriter pw) { - logAndPrintln(pw, "Setting window tracing log frequency to " - + ((onFrame) ? "frame" : "transaction")); - mLogOnFrame = onFrame; - } - - private void setBufferCapacity(int capacity, PrintWriter pw) { - logAndPrintln(pw, "Setting window tracing buffer capacity to " + capacity + "bytes"); - mBuffer.setCapacity(capacity); - } - - boolean isEnabled() { - return mEnabledLockFree; - } - - int onShellCommand(ShellCommand shell) { - PrintWriter pw = shell.getOutPrintWriter(); - String cmd = shell.getNextArgRequired(); - switch (cmd) { - case "start": - startTrace(pw); - return 0; - case "stop": - stopTrace(pw); - return 0; - case "save-for-bugreport": - saveForBugreport(pw); - return 0; - case "status": - logAndPrintln(pw, getStatus()); - return 0; - case "frame": - setLogFrequency(true /* onFrame */, pw); - mBuffer.resetBuffer(); - return 0; - case "transaction": - setLogFrequency(false /* onFrame */, pw); - mBuffer.resetBuffer(); - return 0; - case "level": - String logLevelStr = shell.getNextArgRequired().toLowerCase(); - switch (logLevelStr) { - case "all": { - setLogLevel(WindowTraceLogLevel.ALL, pw); - break; - } - case "trim": { - setLogLevel(WindowTraceLogLevel.TRIM, pw); - break; - } - case "critical": { - setLogLevel(WindowTraceLogLevel.CRITICAL, pw); - break; - } - default: { - setLogLevel(WindowTraceLogLevel.TRIM, pw); - break; - } - } - mBuffer.resetBuffer(); - return 0; - case "size": - setBufferCapacity(Integer.parseInt(shell.getNextArgRequired()) * 1024, pw); - mBuffer.resetBuffer(); - return 0; - default: - pw.println("Unknown command: " + cmd); - pw.println("Window manager trace options:"); - pw.println(" start: Start logging"); - pw.println(" stop: Stop logging"); - pw.println(" save-for-bugreport: Save logging data to file if it's running."); - pw.println(" frame: Log trace once per frame"); - pw.println(" transaction: Log each transaction"); - pw.println(" size: Set the maximum log size (in KB)"); - pw.println(" status: Print trace status"); - pw.println(" level [lvl]: Set the log level between"); - pw.println(" lvl may be one of:"); - pw.println(" critical: Only visible windows with reduced information"); - pw.println(" trim: All windows with reduced"); - pw.println(" all: All window and information"); - return -1; + if (!android.tracing.Flags.perfettoProtologTracing() + && ProtoLog.getSingleInstance().isProtoEnabled()) { + ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).stopProtoLog(pw, true); + ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw); } + saveForBugreportInternal(pw); } - String getStatus() { - return "Status: " - + ((isEnabled()) ? "Enabled" : "Disabled") - + "\n" - + "Log level: " - + mLogLevel - + "\n" - + mBuffer.getStatus(); - } + abstract void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw); + abstract void setLogFrequency(boolean onFrame, PrintWriter pw); + abstract void setBufferCapacity(int capacity, PrintWriter pw); + abstract boolean isEnabled(); + abstract int onShellCommand(ShellCommand shell); + abstract String getStatus(); /** * If tracing is enabled, log the current state or schedule the next frame to be logged, - * according to {@link #mLogOnFrame}. + * according to the configuration in the derived tracing class. * * @param where Logging point descriptor */ @@ -298,59 +127,63 @@ class WindowTracing { return; } - if (mLogOnFrame) { - schedule(); - } else { + if (shouldLogOnTransaction()) { log(where); } + + if (shouldLogOnFrame()) { + schedule(); + } } /** * Schedule the log to trace the next frame */ private void schedule() { - if (mScheduled) { + if (!mScheduled.compareAndSet(false, true)) { return; } - mScheduled = true; mChoreographer.postFrameCallback(mFrameCallback); } /** - * Write the current frame to the buffer + * Write the current frame to proto * + * @param os Proto stream buffer + * @param logLevel Log level * @param where Logging point descriptor + * @param elapsedRealtimeNanos Timestamp */ - private void log(String where) { + protected void dumpToProto(ProtoOutputStream os, @WindowTraceLogLevel int logLevel, + String where, long elapsedRealtimeNanos) { Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "traceStateLocked"); try { - ProtoOutputStream os = new ProtoOutputStream(); - long tokenOuter = os.start(ENTRY); - os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos()); + os.write(ELAPSED_REALTIME_NANOS, elapsedRealtimeNanos); os.write(WHERE, where); - long tokenInner = os.start(WINDOW_MANAGER_SERVICE); + long token = os.start(WINDOW_MANAGER_SERVICE); synchronized (mGlobalLock) { Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "dumpDebugLocked"); try { - mService.dumpDebugLocked(os, mLogLevel); + mService.dumpDebugLocked(os, logLevel); } finally { Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } } - os.end(tokenInner); - os.end(tokenOuter); - mBuffer.add(os); - mScheduled = false; + os.end(token); } catch (Exception e) { Log.wtf(TAG, "Exception while tracing state", e); } finally { + boolean isOnFrameLogEvent = where == WHERE_ON_FRAME; + if (isOnFrameLogEvent) { + mScheduled.set(false); + } Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } } - private void logAndPrintln(@Nullable PrintWriter pw, String msg) { + protected void logAndPrintln(@Nullable PrintWriter pw, String msg) { Log.i(TAG, msg); if (pw != null) { pw.println(msg); @@ -358,24 +191,10 @@ class WindowTracing { } } - /** - * Writes the trace buffer to disk. This method has no internal synchronization and should be - * externally synchronized - */ - private void writeTraceToFileLocked() { - try { - Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeTraceToFileLocked"); - ProtoOutputStream proto = new ProtoOutputStream(); - proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); - long timeOffsetNs = - TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) - - SystemClock.elapsedRealtimeNanos(); - proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs); - mBuffer.writeTraceToFile(mTraceFile, proto); - } catch (IOException e) { - Log.e(TAG, "Unable to write buffer to file", e); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); - } - } + protected abstract void startTraceInternal(@Nullable PrintWriter pw); + protected abstract void stopTraceInternal(@Nullable PrintWriter pw); + protected abstract void saveForBugreportInternal(@Nullable PrintWriter pw); + protected abstract void log(String where); + protected abstract boolean shouldLogOnFrame(); + protected abstract boolean shouldLogOnTransaction(); } diff --git a/services/core/java/com/android/server/wm/WindowTracingLegacy.java b/services/core/java/com/android/server/wm/WindowTracingLegacy.java new file mode 100644 index 000000000000..7a36707fd95a --- /dev/null +++ b/services/core/java/com/android/server/wm/WindowTracingLegacy.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.server.wm.WindowManagerTraceFileProto.ENTRY; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L; +import static com.android.server.wm.WindowManagerTraceFileProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS; + +import android.annotation.Nullable; +import android.os.ShellCommand; +import android.os.SystemClock; +import android.os.Trace; +import android.util.Log; +import android.util.proto.ProtoOutputStream; +import android.view.Choreographer; + +import com.android.internal.util.TraceBuffer; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.concurrent.TimeUnit; + +class WindowTracingLegacy extends WindowTracing { + + /** + * Maximum buffer size, currently defined as 5 MB + * Size was experimentally defined to fit between 100 to 150 elements. + */ + private static final int BUFFER_CAPACITY_CRITICAL = 5120 * 1024; // 5 MB + private static final int BUFFER_CAPACITY_TRIM = 10240 * 1024; // 10 MB + private static final int BUFFER_CAPACITY_ALL = 20480 * 1024; // 20 MB + static final String WINSCOPE_EXT = ".winscope"; + private static final String TRACE_FILENAME = "/data/misc/wmtrace/wm_trace" + WINSCOPE_EXT; + private static final String TAG = "WindowTracing"; + private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; + + private final Object mEnabledLock = new Object(); + private final File mTraceFile; + private final TraceBuffer mBuffer; + + private boolean mEnabled; + private volatile boolean mEnabledLockFree; + + protected @WindowTraceLogLevel int mLogLevel = WindowTraceLogLevel.TRIM; + protected boolean mLogOnFrame = false; + + WindowTracingLegacy(WindowManagerService service, Choreographer choreographer) { + this(new File(TRACE_FILENAME), service, choreographer, + service.mGlobalLock, BUFFER_CAPACITY_TRIM); + } + + WindowTracingLegacy(File traceFile, WindowManagerService service, Choreographer choreographer, + WindowManagerGlobalLock globalLock, int bufferSize) { + super(service, choreographer, globalLock); + mTraceFile = traceFile; + mBuffer = new TraceBuffer(bufferSize); + } + + @Override + void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw) { + logAndPrintln(pw, "Setting window tracing log level to " + logLevel); + mLogLevel = logLevel; + + switch (logLevel) { + case WindowTraceLogLevel.ALL: { + setBufferCapacity(BUFFER_CAPACITY_ALL, pw); + break; + } + case WindowTraceLogLevel.TRIM: { + setBufferCapacity(BUFFER_CAPACITY_TRIM, pw); + break; + } + case WindowTraceLogLevel.CRITICAL: { + setBufferCapacity(BUFFER_CAPACITY_CRITICAL, pw); + break; + } + } + } + + @Override + void setLogFrequency(boolean onFrame, PrintWriter pw) { + logAndPrintln(pw, "Setting window tracing log frequency to " + + ((onFrame) ? "frame" : "transaction")); + mLogOnFrame = onFrame; + } + + @Override + void setBufferCapacity(int capacity, PrintWriter pw) { + logAndPrintln(pw, "Setting window tracing buffer capacity to " + capacity + "bytes"); + mBuffer.setCapacity(capacity); + } + + @Override + boolean isEnabled() { + return mEnabledLockFree; + } + + @Override + int onShellCommand(ShellCommand shell) { + PrintWriter pw = shell.getOutPrintWriter(); + String cmd = shell.getNextArgRequired(); + switch (cmd) { + case "start": + startTrace(pw); + return 0; + case "stop": + stopTrace(pw); + return 0; + case "save-for-bugreport": + saveForBugreport(pw); + return 0; + case "status": + logAndPrintln(pw, getStatus()); + return 0; + case "frame": + setLogFrequency(true /* onFrame */, pw); + mBuffer.resetBuffer(); + return 0; + case "transaction": + setLogFrequency(false /* onFrame */, pw); + mBuffer.resetBuffer(); + return 0; + case "level": + String logLevelStr = shell.getNextArgRequired().toLowerCase(); + switch (logLevelStr) { + case "all": { + setLogLevel(WindowTraceLogLevel.ALL, pw); + break; + } + case "trim": { + setLogLevel(WindowTraceLogLevel.TRIM, pw); + break; + } + case "critical": { + setLogLevel(WindowTraceLogLevel.CRITICAL, pw); + break; + } + default: { + setLogLevel(WindowTraceLogLevel.TRIM, pw); + break; + } + } + mBuffer.resetBuffer(); + return 0; + case "size": + setBufferCapacity(Integer.parseInt(shell.getNextArgRequired()) * 1024, pw); + mBuffer.resetBuffer(); + return 0; + default: + pw.println("Unknown command: " + cmd); + pw.println("Window manager trace options:"); + pw.println(" start: Start logging"); + pw.println(" stop: Stop logging"); + pw.println(" save-for-bugreport: Save logging data to file if it's running."); + pw.println(" frame: Log trace once per frame"); + pw.println(" transaction: Log each transaction"); + pw.println(" size: Set the maximum log size (in KB)"); + pw.println(" status: Print trace status"); + pw.println(" level [lvl]: Set the log level between"); + pw.println(" lvl may be one of:"); + pw.println(" critical: Only visible windows with reduced information"); + pw.println(" trim: All windows with reduced"); + pw.println(" all: All window and information"); + return -1; + } + } + + @Override + String getStatus() { + return "Status: " + + ((isEnabled()) ? "Enabled" : "Disabled") + + "\n" + + "Log level: " + + mLogLevel + + "\n" + + mBuffer.getStatus(); + } + + @Override + protected void startTraceInternal(@Nullable PrintWriter pw) { + synchronized (mEnabledLock) { + logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); + mBuffer.resetBuffer(); + mEnabled = mEnabledLockFree = true; + } + log(WHERE_START_TRACING); + } + + @Override + protected void stopTraceInternal(@Nullable PrintWriter pw) { + synchronized (mEnabledLock) { + logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush."); + mEnabled = mEnabledLockFree = false; + + if (mEnabled) { + logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush."); + throw new IllegalStateException("tracing enabled while waiting for flush."); + } + writeTraceToFileLocked(); + logAndPrintln(pw, "Trace written to " + mTraceFile + "."); + } + } + + @Override + protected void saveForBugreportInternal(@Nullable PrintWriter pw) { + synchronized (mEnabledLock) { + if (!mEnabled) { + return; + } + mEnabled = mEnabledLockFree = false; + logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush."); + writeTraceToFileLocked(); + logAndPrintln(pw, "Trace written to " + mTraceFile + "."); + logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); + mBuffer.resetBuffer(); + mEnabled = mEnabledLockFree = true; + } + } + + @Override + protected void log(String where) { + try { + ProtoOutputStream os = new ProtoOutputStream(); + long token = os.start(ENTRY); + dumpToProto(os, mLogLevel, where, SystemClock.elapsedRealtimeNanos()); + os.end(token); + mBuffer.add(os); + } catch (Exception e) { + Log.wtf(TAG, "Exception while tracing state", e); + } + } + + @Override + protected boolean shouldLogOnFrame() { + return mLogOnFrame; + } + + @Override + protected boolean shouldLogOnTransaction() { + return !mLogOnFrame; + } + + private void writeTraceToFileLocked() { + try { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeTraceToFileLocked"); + ProtoOutputStream proto = new ProtoOutputStream(); + proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); + long timeOffsetNs = + TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) + - SystemClock.elapsedRealtimeNanos(); + proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs); + mBuffer.writeTraceToFile(mTraceFile, proto); + } catch (IOException e) { + Log.e(TAG, "Unable to write buffer to file", e); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } +} diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 1f0c827083ac..5719810abc5a 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -314,6 +314,7 @@ public: void getReaderConfiguration(InputReaderConfiguration* outConfig) override; void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices) override; + void notifyConfigurationChanged(nsecs_t when) override; std::shared_ptr<KeyCharacterMap> getKeyboardLayoutOverlay( const InputDeviceIdentifier& identifier, const std::optional<KeyboardLayoutInfo> keyboardLayoutInfo) override; @@ -331,7 +332,6 @@ public: void notifySwitch(nsecs_t when, uint32_t switchValues, uint32_t switchMask, uint32_t policyFlags) override; - void notifyConfigurationChanged(nsecs_t when) override; // ANR-related callbacks -- start void notifyNoFocusedWindowAnr(const std::shared_ptr<InputApplicationHandle>& handle) override; void notifyWindowUnresponsive(const sp<IBinder>& token, std::optional<gui::Pid> pid, @@ -382,6 +382,7 @@ public: PointerControllerInterface::ControllerType type) override; void notifyPointerDisplayIdChanged(ui::LogicalDisplayId displayId, const FloatPoint& position) override; + void notifyMouseCursorFadedOnTyping() override; /* --- InputFilterPolicyInterface implementation --- */ void notifyStickyModifierStateChanged(uint32_t modifierState, @@ -788,6 +789,10 @@ void NativeInputManager::notifyPointerDisplayIdChanged(ui::LogicalDisplayId poin InputReaderConfiguration::Change::DISPLAY_INFO); } +void NativeInputManager::notifyMouseCursorFadedOnTyping() { + mInputManager->getReader().notifyMouseCursorFadedOnTyping(); +} + void NativeInputManager::notifyStickyModifierStateChanged(uint32_t modifierState, uint32_t lockedModifierState) { JNIEnv* env = jniEnv(); @@ -1847,10 +1852,6 @@ static void handleInputChannelDisposed(JNIEnv* env, jobject /* inputChannelObj * const std::shared_ptr<InputChannel>& inputChannel, void* data) { NativeInputManager* im = static_cast<NativeInputManager*>(data); - - ALOGW("Input channel object '%s' was disposed without first being removed with " - "the input manager!", - inputChannel->getName().c_str()); im->removeInputChannel(inputChannel->getConnectionToken()); } diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index eeb8b9b0b469..ec7406ae6219 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -315,6 +315,42 @@ <xs:element name="screenBrightnessRampDecrease" type="nonNegativeDecimal"> <xs:annotation name="final"/> </xs:element> + + <!-- The minimum HDR layer % at which hdr boost will be applied with transition point cap. + Should be lower or equal to minimumHdrPercentOfScreenForHbm. --> + <xs:element name="minimumHdrPercentOfScreenForNbm" type="nonNegativeDecimal" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="nullable"/> + <xs:annotation name="final"/> + </xs:element> + <!-- The minimum HDR layer size at which hdr boost will be applied. --> + <xs:element name="minimumHdrPercentOfScreenForHbm" type="nonNegativeDecimal" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="nullable"/> + <xs:annotation name="final"/> + </xs:element> + <!-- If hdr boost is allowed in low power mode. --> + <xs:element name="allowInLowPowerMode" type="xs:boolean" minOccurs="0" maxOccurs="1"> + <xs:annotation name="nonnull"/> + <xs:annotation name="final"/> + </xs:element> + <!-- sdrNits, hdrRatio This LUT specifies how to boost HDR brightness at given SDR brightness (nits). --> + <!-- sdr brightness to hdr boost ratio map + Format: first = sdrNits, second = hdrRatio. E.g. : + <sdrHdrRatioMap> + <point> + <first>2.0</first> // sdrNits + <second>4.0</second> // hdrRatio + </point> + .... + </sdrHdrRatioMap> + --> + <xs:element type="nonNegativeFloatToFloatMap" name="sdrHdrRatioMap" minOccurs="0" maxOccurs="1"> + <xs:annotation name="nullable"/> + <xs:annotation name="final"/> + </xs:element> + + </xs:complexType> <!-- Maps to PowerManager.THERMAL_STATUS_* values. --> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index 757b23a2df7e..68d74cf723e1 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -226,16 +226,24 @@ package com.android.server.display.config { public class HdrBrightnessConfig { ctor public HdrBrightnessConfig(); + method @NonNull public final boolean getAllowInLowPowerMode(); method public final java.math.BigInteger getBrightnessDecreaseDebounceMillis(); method public final java.math.BigInteger getBrightnessIncreaseDebounceMillis(); method @NonNull public final com.android.server.display.config.NonNegativeFloatToFloatMap getBrightnessMap(); + method @Nullable public final java.math.BigDecimal getMinimumHdrPercentOfScreenForHbm(); + method @Nullable public final java.math.BigDecimal getMinimumHdrPercentOfScreenForNbm(); method public final java.math.BigDecimal getScreenBrightnessRampDecrease(); method public final java.math.BigDecimal getScreenBrightnessRampIncrease(); + method @Nullable public final com.android.server.display.config.NonNegativeFloatToFloatMap getSdrHdrRatioMap(); + method public final void setAllowInLowPowerMode(@NonNull boolean); method public final void setBrightnessDecreaseDebounceMillis(java.math.BigInteger); method public final void setBrightnessIncreaseDebounceMillis(java.math.BigInteger); method public final void setBrightnessMap(@NonNull com.android.server.display.config.NonNegativeFloatToFloatMap); + method public final void setMinimumHdrPercentOfScreenForHbm(@Nullable java.math.BigDecimal); + method public final void setMinimumHdrPercentOfScreenForNbm(@Nullable java.math.BigDecimal); method public final void setScreenBrightnessRampDecrease(java.math.BigDecimal); method public final void setScreenBrightnessRampIncrease(java.math.BigDecimal); + method public final void setSdrHdrRatioMap(@Nullable com.android.server.display.config.NonNegativeFloatToFloatMap); } public class HighBrightnessMode { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index e5a1ebfdc404..db4b171152a7 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -381,6 +381,9 @@ public final class SystemServer implements Dumpable { + "OnDevicePersonalizationSystemService$Lifecycle"; private static final String UPDATABLE_DEVICE_CONFIG_SERVICE_CLASS = "com.android.server.deviceconfig.DeviceConfigInit$Lifecycle"; + private static final String CRASHRECOVERY_MODULE_LIFECYCLE_CLASS = + "com.android.server.crashrecovery.CrashRecoveryModule$Lifecycle"; + /* * Implementation class names and jar locations for services in @@ -1196,15 +1199,18 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(RecoverySystemService.Lifecycle.class); t.traceEnd(); - // Initialize RescueParty. - RescueParty.registerHealthObserver(mSystemContext); - if (!Flags.recoverabilityDetection()) { - // Now that we have the bare essentials of the OS up and running, take - // note that we just booted, which might send out a rescue party if - // we're stuck in a runtime restart loop. - PackageWatchdog.getInstance(mSystemContext).noteBoot(); + if (!Flags.refactorCrashrecovery()) { + // Initialize RescueParty. + RescueParty.registerHealthObserver(mSystemContext); + if (!Flags.recoverabilityDetection()) { + // Now that we have the bare essentials of the OS up and running, take + // note that we just booted, which might send out a rescue party if + // we're stuck in a runtime restart loop. + PackageWatchdog.getInstance(mSystemContext).noteBoot(); + } } + // Manages LEDs and display backlight so we need it to bring up the display. t.traceBegin("StartLightsService"); mSystemServiceManager.startService(LightsService.class); @@ -2931,12 +2937,18 @@ public final class SystemServer implements Dumpable { mPackageManagerService.systemReady(); t.traceEnd(); - if (Flags.recoverabilityDetection()) { - // Now that we have the essential services needed for mitigations, register the boot - // with package watchdog. - // Note that we just booted, which might send out a rescue party if we're stuck in a - // runtime restart loop. - PackageWatchdog.getInstance(mSystemContext).noteBoot(); + if (Flags.refactorCrashrecovery()) { + t.traceBegin("StartCrashRecoveryModule"); + mSystemServiceManager.startService(CRASHRECOVERY_MODULE_LIFECYCLE_CLASS); + t.traceEnd(); + } else { + if (Flags.recoverabilityDetection()) { + // Now that we have the essential services needed for mitigations, register the boot + // with package watchdog. + // Note that we just booted, which might send out a rescue party if we're stuck in a + // runtime restart loop. + PackageWatchdog.getInstance(mSystemContext).noteBoot(); + } } t.traceBegin("MakeDisplayManagerServiceReady"); diff --git a/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt b/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt index 6b20ef1d5519..996daf5a5f68 100644 --- a/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt +++ b/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt @@ -47,9 +47,8 @@ inline fun AtomicFile.readWithReserveCopy(block: (FileInputStream) -> Unit) { /** Write to actual file and reserve file. */ @Throws(IOException::class) inline fun AtomicFile.writeWithReserveCopy(block: (FileOutputStream) -> Unit) { - val reserveFile = File(baseFile.parentFile, baseFile.name + ".reservecopy") - reserveFile.delete() writeInlined(block) + val reserveFile = File(baseFile.parentFile, baseFile.name + ".reservecopy") try { FileInputStream(baseFile).use { inputStream -> FileOutputStream(reserveFile).use { outputStream -> 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 6a86379f1ad5..bacde101bf36 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -25,6 +25,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.notNull; @@ -38,6 +39,7 @@ import android.app.ActivityManagerInternal; import android.content.BroadcastReceiver; import android.content.Context; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.res.Configuration; import android.hardware.input.IInputManager; @@ -157,6 +159,7 @@ public class InputMethodManagerServiceTestBase { mockitoSession() .initMocks(this) .strictness(Strictness.LENIENT) + .spyStatic(InputMethodUtils.class) .mockStatic(ServiceManager.class) .mockStatic(SystemServerInitThreadPool.class) .startMocking(); @@ -227,6 +230,10 @@ public class InputMethodManagerServiceTestBase { .thenReturn(TEST_IME_TARGET_INFO); when(mMockInputMethodClient.asBinder()).thenReturn(mMockInputMethodBinder); + // This changes the real IME component state. Not appropriate to do in tests. + doNothing().when(() -> InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( + any(PackageManager.class), anyList())); + // Used by lazy initializing draw IMS nav bar at InputMethodManagerService#systemRunning(), // which is ok to be mocked out for now. doReturn(null).when(() -> SystemServerInitThreadPool.submit(any(), anyString())); @@ -243,9 +250,13 @@ public class InputMethodManagerServiceTestBase { Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */); mIoThread.start(); + + final var ioHandler = spy(Handler.createAsync(mIoThread.getLooper())); + doReturn(true).when(ioHandler).post(any()); + mInputMethodManagerService = new InputMethodManagerService(mContext, InputMethodManagerService.shouldEnableConcurrentMultiUserMode(mContext), - mServiceThread.getLooper(), Handler.createAsync(mIoThread.getLooper()), + mServiceThread.getLooper(), ioHandler, unusedUserId -> mMockInputMethodBindingController); spyOn(mInputMethodManagerService); @@ -257,16 +268,9 @@ public class InputMethodManagerServiceTestBase { // Public local InputMethodManagerService. LocalServices.removeServiceForTest(InputMethodManagerInternal.class); lifecycle.onStart(); - try { - // After this boot phase, services can broadcast Intents. - lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); - } catch (SecurityException e) { - // Security exception to permission denial is expected in test, mocking out to ensure - // InputMethodManagerService as system ready state. - if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) { - throw e; - } - } + + // After this boot phase, services can broadcast Intents. + lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); // Call InputMethodManagerService#addClient() as a preparation to start interacting with it. mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection, 0); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java index 9ca4f1daa46c..81fb1a092887 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java @@ -18,23 +18,12 @@ package com.android.server.inputmethod; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import android.content.pm.UserInfo; -import android.os.ConditionVariable; -import android.os.Handler; -import android.os.Looper; import android.platform.test.ravenwood.RavenwoodRule; -import com.android.server.pm.UserManagerInternal; - import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -52,13 +41,8 @@ public final class UserDataRepositoryTest { .setProvideMainThread(true).build(); @Mock - private UserManagerInternal mMockUserManagerInternal; - - @Mock private InputMethodManagerService mMockInputMethodManagerService; - private Handler mHandler; - private IntFunction<InputMethodBindingController> mBindingControllerFactory; @Before @@ -66,7 +50,6 @@ public final class UserDataRepositoryTest { MockitoAnnotations.initMocks(this); SecureSettingsWrapper.startTestMode(); - mHandler = new Handler(Looper.getMainLooper()); mBindingControllerFactory = new IntFunction<InputMethodBindingController>() { @Override @@ -81,51 +64,20 @@ public final class UserDataRepositoryTest { SecureSettingsWrapper.endTestMode(); } - @Test - public void testUserDataRepository_addsNewUserInfoOnUserCreatedEvent() { - // Create UserDataRepository and capture the user lifecycle listener - final var captor = ArgumentCaptor.forClass(UserManagerInternal.UserLifecycleListener.class); - final var bindingControllerFactorySpy = spy(mBindingControllerFactory); - final var repository = new UserDataRepository(mHandler, - mMockUserManagerInternal, bindingControllerFactorySpy); - - verify(mMockUserManagerInternal, times(1)).addUserLifecycleListener(captor.capture()); - final var listener = captor.getValue(); - - // Assert that UserDataRepository is empty and then call onUserCreated - assertThat(collectUserData(repository)).isEmpty(); - final var userInfo = new UserInfo(); - userInfo.id = ANY_USER_ID; - listener.onUserCreated(userInfo, /* unused token */ new Object()); - waitForIdle(); - - // Assert UserDataRepository remains to be empty. - assertThat(collectUserData(repository)).isEmpty(); - } - + // TODO(b/352615651): Move this to end-to-end test. @Test public void testUserDataRepository_removesUserInfoOnUserRemovedEvent() { - // Create UserDataRepository and capture the user lifecycle listener - final var captor = ArgumentCaptor.forClass(UserManagerInternal.UserLifecycleListener.class); - final var repository = new UserDataRepository(mHandler, - mMockUserManagerInternal, + // Create UserDataRepository + final var repository = new UserDataRepository( userId -> new InputMethodBindingController(userId, mMockInputMethodManagerService)); - verify(mMockUserManagerInternal, times(1)).addUserLifecycleListener(captor.capture()); - final var listener = captor.getValue(); - // Add one UserData ... - synchronized (ImfLock.class) { - final var userData = repository.getOrCreate(ANY_USER_ID); - assertThat(userData.mUserId).isEqualTo(ANY_USER_ID); - } + final var userData = repository.getOrCreate(ANY_USER_ID); + assertThat(userData.mUserId).isEqualTo(ANY_USER_ID); // ... and then call onUserRemoved assertThat(collectUserData(repository)).hasSize(1); - final var userInfo = new UserInfo(); - userInfo.id = ANY_USER_ID; - listener.onUserRemoved(userInfo); - waitForIdle(); + repository.remove(ANY_USER_ID); // Assert UserDataRepository is now empty assertThat(collectUserData(repository)).isEmpty(); @@ -133,13 +85,10 @@ public final class UserDataRepositoryTest { @Test public void testGetOrCreate() { - final var repository = new UserDataRepository(mHandler, - mMockUserManagerInternal, mBindingControllerFactory); + final var repository = new UserDataRepository(mBindingControllerFactory); - synchronized (ImfLock.class) { - final var userData = repository.getOrCreate(ANY_USER_ID); - assertThat(userData.mUserId).isEqualTo(ANY_USER_ID); - } + final var userData = repository.getOrCreate(ANY_USER_ID); + assertThat(userData.mUserId).isEqualTo(ANY_USER_ID); final var allUserData = collectUserData(repository); assertThat(allUserData).hasSize(1); @@ -151,15 +100,8 @@ public final class UserDataRepositoryTest { private List<UserDataRepository.UserData> collectUserData(UserDataRepository repository) { final var collected = new ArrayList<UserDataRepository.UserData>(); - synchronized (ImfLock.class) { - repository.forAllUserData(userData -> collected.add(userData)); - } + repository.forAllUserData(userData -> collected.add(userData)); return collected; } - private void waitForIdle() { - final var done = new ConditionVariable(); - mHandler.post(done::open); - done.block(); - } } diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java index 89b4aea216f9..71383069d08a 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java @@ -1027,6 +1027,25 @@ public class PackageManagerSettingsTests { } @Test + public void testWriteReadDebuggable() { + Settings settings = makeSettings(); + PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); + packageSetting.setAppId(Process.FIRST_APPLICATION_UID); + packageSetting.setPkg(PackageImpl.forTesting(PACKAGE_NAME_1).hideAsParsed() + .setUid(packageSetting.getAppId()) + .hideAsFinal()); + + packageSetting.setDebuggable(true); + settings.mPackages.put(PACKAGE_NAME_1, packageSetting); + + settings.writeLPr(computer, /* sync= */ true); + settings.mPackages.clear(); + + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + assertThat(settings.getPackageLPr(PACKAGE_NAME_1).isDebuggable(), is(true)); + } + + @Test public void testWriteReadArchiveState() { Settings settings = makeSettings(); PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index 9a25b1acfaae..343792336247 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -54,6 +54,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.server.display.config.HdrBrightnessData; +import com.android.server.display.config.HighBrightnessModeData; import com.android.server.display.config.HysteresisLevels; import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint; import com.android.server.display.config.RefreshRateData; @@ -436,7 +437,7 @@ public final class DisplayDeviceConfigTest { public void testHighBrightnessModeDataFromDisplayConfig() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile(); - DisplayDeviceConfig.HighBrightnessModeData hbmData = + HighBrightnessModeData hbmData = mDisplayDeviceConfig.getHighBrightnessModeData(); assertNotNull(hbmData); assertEquals(BRIGHTNESS[1], hbmData.transitionPoint, ZERO_DELTA); @@ -671,14 +672,14 @@ public final class DisplayDeviceConfigTest { HdrBrightnessData data = mDisplayDeviceConfig.getHdrBrightnessData(); assertNotNull(data); - assertEquals(2, data.mMaxBrightnessLimits.size()); - assertEquals(13000, data.mBrightnessDecreaseDebounceMillis); - assertEquals(0.1f, data.mScreenBrightnessRampDecrease, SMALL_DELTA); - assertEquals(1000, data.mBrightnessIncreaseDebounceMillis); - assertEquals(0.11f, data.mScreenBrightnessRampIncrease, SMALL_DELTA); - - assertEquals(0.3f, data.mMaxBrightnessLimits.get(500f), SMALL_DELTA); - assertEquals(0.6f, data.mMaxBrightnessLimits.get(1200f), SMALL_DELTA); + assertEquals(2, data.maxBrightnessLimits.size()); + assertEquals(13000, data.brightnessDecreaseDebounceMillis); + assertEquals(0.1f, data.screenBrightnessRampDecrease, SMALL_DELTA); + assertEquals(1000, data.brightnessIncreaseDebounceMillis); + assertEquals(0.11f, data.screenBrightnessRampIncrease, SMALL_DELTA); + + assertEquals(0.3f, data.maxBrightnessLimits.get(500f), SMALL_DELTA); + assertEquals(0.6f, data.maxBrightnessLimits.get(1200f), SMALL_DELTA); } private void verifyConfigValuesFromConfigResource() { diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 2018e1a3ae62..5c291569b8d8 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -83,6 +83,7 @@ import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.clamper.BrightnessClamperController; import com.android.server.display.brightness.clamper.HdrClamper; import com.android.server.display.color.ColorDisplayService; +import com.android.server.display.config.HighBrightnessModeData; import com.android.server.display.config.HysteresisLevels; import com.android.server.display.config.SensorData; import com.android.server.display.feature.DisplayManagerFlags; @@ -2425,7 +2426,7 @@ public final class DisplayPowerControllerTest { @Override HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken, String displayUniqueId, float brightnessMin, - float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData, + float brightnessMax, HighBrightnessModeData hbmData, HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg, Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata, Context context) { diff --git a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java index 8e01a11cb23f..cde87b9b89b2 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java @@ -23,7 +23,6 @@ import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLI import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE; -import static com.android.server.display.DisplayDeviceConfig.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT; import static com.android.server.display.HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID; import static org.junit.Assert.assertEquals; @@ -56,8 +55,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; -import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData; import com.android.server.display.HighBrightnessModeController.Injector; +import com.android.server.display.config.HighBrightnessModeData; import com.android.server.testutils.OffsettableClock; import org.junit.Before; @@ -77,6 +76,7 @@ public class HighBrightnessModeControllerTest { private static final long TIME_ALLOWED_IN_WINDOW_MILLIS = 12 * 1000; private static final long TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS = 5 * 1000; private static final boolean ALLOW_IN_LOW_POWER_MODE = false; + private static final float HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT = 0.5f; private static final float DEFAULT_MIN = 0.01f; private static final float DEFAULT_MAX = 0.80f; @@ -103,7 +103,8 @@ public class HighBrightnessModeControllerTest { private static final HighBrightnessModeData DEFAULT_HBM_DATA = new HighBrightnessModeData(MINIMUM_LUX, TRANSITION_POINT, TIME_WINDOW_MILLIS, TIME_ALLOWED_IN_WINDOW_MILLIS, TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS, - ALLOW_IN_LOW_POWER_MODE, HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT); + ALLOW_IN_LOW_POWER_MODE, HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT, + null, null, true); @Before public void setUp() { diff --git a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java index 7e7ccf733876..7132bc1687ca 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java @@ -22,6 +22,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; +import com.android.server.display.config.HighBrightnessModeData; + import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -39,7 +41,7 @@ public class HighBrightnessModeMetadataMapperTest { private DisplayDeviceConfig mDdcMock; @Mock - private DisplayDeviceConfig.HighBrightnessModeData mHbmDataMock; + private HighBrightnessModeData mHbmDataMock; private HighBrightnessModeMetadataMapper mHighBrightnessModeMetadataMapper; diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index 6d138c5b6b29..1729ad5ff19f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -16,7 +16,7 @@ package com.android.server.display; -import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.DEFAULT_DISPLAY_GROUP; import static android.view.Display.FLAG_REAR; @@ -62,9 +62,11 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.annotation.NonNull; import android.app.PropertyInvalidatedCache; import android.content.Context; import android.content.res.Resources; +import android.hardware.devicestate.DeviceState; import android.os.Handler; import android.os.IPowerManager; import android.os.IThermalService; @@ -103,7 +105,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Set; @SmallTest @RunWith(AndroidJUnit4.class) @@ -111,9 +115,12 @@ public class LogicalDisplayMapperTest { private static int sUniqueTestDisplayId = 0; private static final int TIMEOUT_STATE_TRANSITION_MILLIS = 500; private static final int FOLD_SETTLE_DELAY = 1000; - private static final int DEVICE_STATE_CLOSED = 0; - private static final int DEVICE_STATE_HALF_OPEN = 1; - private static final int DEVICE_STATE_OPEN = 2; + private static final DeviceState DEVICE_STATE_CLOSED = createDeviceState(0, "Zero", + Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP), Collections.emptySet()); + private static final DeviceState DEVICE_STATE_HALF_OPEN = createDeviceState(1, "One", + Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet()); + private static final DeviceState DEVICE_STATE_OPEN = createDeviceState(2, "Two", + Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet()); private static final int FLAG_GO_TO_SLEEP_ON_FOLD = 0; private static final int FLAG_GO_TO_SLEEP_FLAG_SOFT_SLEEP = 2; private static int sNextNonDefaultDisplayId = DEFAULT_DISPLAY + 1; @@ -703,8 +710,7 @@ public class LogicalDisplayMapperTest { /* isInteractive= */true, /* isBootCompleted= */true)); assertFalse(mLogicalDisplayMapper.shouldDeviceBePutToSleep(DEVICE_STATE_CLOSED, - INVALID_DEVICE_STATE_IDENTIFIER, - /* isInteractive= */true, + INVALID_DEVICE_STATE /* currentState */, /* isInteractive= */true, /* isBootCompleted= */true)); } @@ -932,7 +938,7 @@ public class LogicalDisplayMapperTest { // We can only have one default display assertEquals(DEFAULT_DISPLAY, id(display1)); - mLogicalDisplayMapper.setDeviceStateLocked(0); + mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_CLOSED); advanceTime(1000); // The new state is not applied until the boot is completed assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked()); @@ -953,7 +959,7 @@ public class LogicalDisplayMapperTest { assertEquals("concurrent", mLogicalDisplayMapper.getDisplayLocked(device2) .getDisplayInfoLocked().thermalBrightnessThrottlingDataId); - mLogicalDisplayMapper.setDeviceStateLocked(1); + mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_HALF_OPEN); advanceTime(1000); assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked()); assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked()); @@ -966,7 +972,7 @@ public class LogicalDisplayMapperTest { mLogicalDisplayMapper.getDisplayLocked(device2) .getDisplayInfoLocked().thermalBrightnessThrottlingDataId); - mLogicalDisplayMapper.setDeviceStateLocked(2); + mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_OPEN); advanceTime(1000); assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked()); assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked()); @@ -1043,7 +1049,7 @@ public class LogicalDisplayMapperTest { // 3) Send DISPLAY_DEVICE_EVENT_CHANGE to inform the mapper of the new display state // 4) Dispatch handler events. mLogicalDisplayMapper.onBootCompleted(); - mLogicalDisplayMapper.setDeviceStateLocked(0); + mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_CLOSED); mDisplayDeviceRepo.onDisplayDeviceEvent(device3, DISPLAY_DEVICE_EVENT_CHANGED); advanceTime(1000); final int[] allDisplayIds = mLogicalDisplayMapper.getDisplayIdsLocked( @@ -1073,7 +1079,7 @@ public class LogicalDisplayMapperTest { /* includeDisabled= */ false)); // Now do it again to go back to state 1 - mLogicalDisplayMapper.setDeviceStateLocked(1); + mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_HALF_OPEN); mDisplayDeviceRepo.onDisplayDeviceEvent(device3, DISPLAY_DEVICE_EVENT_CHANGED); advanceTime(1000); final int[] threeDisplaysEnabled = mLogicalDisplayMapper.getDisplayIdsLocked( @@ -1127,7 +1133,7 @@ public class LogicalDisplayMapperTest { // We can only have one default display assertEquals(DEFAULT_DISPLAY, id(display1)); - mLogicalDisplayMapper.setDeviceStateLocked(0); + mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_CLOSED); advanceTime(1000); mLogicalDisplayMapper.onBootCompleted(); advanceTime(1000); @@ -1180,13 +1186,15 @@ public class LogicalDisplayMapperTest { Layout layout = new Layout(); createDefaultDisplay(layout, outer); createNonDefaultDisplay(layout, inner, /* enabled= */ false, /* group= */ null); - when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_CLOSED)).thenReturn(layout); + when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_CLOSED.getIdentifier())).thenReturn( + layout); layout = new Layout(); createNonDefaultDisplay(layout, outer, /* enabled= */ false, /* group= */ null); createDefaultDisplay(layout, inner); - when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_HALF_OPEN)).thenReturn(layout); - when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_OPEN)).thenReturn(layout); + when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_HALF_OPEN.getIdentifier())).thenReturn( + layout); + when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_OPEN.getIdentifier())).thenReturn(layout); when(mDeviceStateToLayoutMapSpy.size()).thenReturn(4); add(outer); @@ -1317,6 +1325,15 @@ public class LogicalDisplayMapperTest { assertNotEquals(DEFAULT_DISPLAY, id(displayRemoved)); } + private static DeviceState createDeviceState(int identifier, @NonNull String name, + @NonNull Set<@DeviceState.SystemDeviceStateProperties Integer> systemProperties, + @NonNull Set<@DeviceState.PhysicalDeviceStateProperties Integer> physicalProperties) { + DeviceState.Configuration deviceStateConfiguration = new DeviceState.Configuration.Builder( + identifier, name).setSystemProperties(systemProperties).setPhysicalProperties( + physicalProperties).build(); + return new DeviceState(deviceStateConfiguration); + } + private final static class FoldableDisplayDevices { final TestDisplayDevice mOuter; final TestDisplayDevice mInner; diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java index c785ea6b1865..721285636d7a 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java @@ -34,6 +34,7 @@ import android.os.PowerManager; import androidx.test.filters.SmallTest; import com.android.server.display.AutomaticBrightnessController; +import com.android.server.display.config.DisplayDeviceConfigTestUtilsKt; import com.android.server.display.config.HdrBrightnessData; import com.android.server.testutils.OffsettableClock; import com.android.server.testutils.TestHandler; @@ -54,13 +55,14 @@ public class HdrClamperTest { private static final float FLOAT_TOLERANCE = 0.0001f; private static final long SEND_TIME_TOLERANCE = 100; - private static final HdrBrightnessData TEST_HDR_DATA = new HdrBrightnessData( - Map.of(500f, 0.6f), - /* brightnessIncreaseDebounceMillis= */ 1000, - /* screenBrightnessRampIncrease= */ 0.02f, - /* brightnessDecreaseDebounceMillis= */ 3000, - /* screenBrightnessRampDecrease= */0.04f - ); + private static final HdrBrightnessData TEST_HDR_DATA = DisplayDeviceConfigTestUtilsKt + .createHdrBrightnessData( + Map.of(500f, 0.6f), + /* brightnessIncreaseDebounceMillis= */ 1000, + /* screenBrightnessRampIncrease= */ 0.02f, + /* brightnessDecreaseDebounceMillis= */ 3000, + /* screenBrightnessRampDecrease= */0.04f + ); private static final int WIDTH = 600; private static final int HEIGHT = 800; diff --git a/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt new file mode 100644 index 000000000000..3b3d6f74da50 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display.config + +import android.util.Spline +import android.util.Xml +import com.android.server.display.config.HighBrightnessModeData.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.OutputStreamWriter +import org.xmlpull.v1.XmlSerializer + +fun createRefreshRateData( + defaultRefreshRate: Int = 60, + defaultPeakRefreshRate: Int = 60, + defaultRefreshRateInHbmHdr: Int = 60, + defaultRefreshRateInHbmSunlight: Int = 60, + lowPowerSupportedModes: List<SupportedModeData> = emptyList(), + lowLightBlockingZoneSupportedModes: List<SupportedModeData> = emptyList() +): RefreshRateData { + return RefreshRateData( + defaultRefreshRate, defaultPeakRefreshRate, + defaultRefreshRateInHbmHdr, defaultRefreshRateInHbmSunlight, + lowPowerSupportedModes, lowLightBlockingZoneSupportedModes + ) +} + +@JvmOverloads +fun createHdrBrightnessData( + maxBrightnessLimits: Map<Float, Float> = mapOf(Pair(500f, 0.6f)), + brightnessIncreaseDebounceMillis: Long = 1000, + screenBrightnessRampIncrease: Float = 0.02f, + brightnessDecreaseDebounceMillis: Long = 3000, + screenBrightnessRampDecrease: Float = 0.04f, + minimumHdrPercentOfScreenForNbm: Float = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT, + minimumHdrPercentOfScreenForHbm: Float = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT, + allowInLowPowerMode: Boolean = false, + sdrToHdrRatioSpline: Spline? = null +): HdrBrightnessData { + return HdrBrightnessData( + maxBrightnessLimits, + brightnessIncreaseDebounceMillis, + screenBrightnessRampIncrease, + brightnessDecreaseDebounceMillis, + screenBrightnessRampDecrease, + minimumHdrPercentOfScreenForNbm, + minimumHdrPercentOfScreenForHbm, + allowInLowPowerMode, + sdrToHdrRatioSpline + ) +} + +fun XmlSerializer.highBrightnessMode( + enabled: String = "true", + transitionPoint: String = "0.67", + minimumLux: String = "2500", + timeWindowSecs: String = "200", + timeMaxSecs: String = "30", + timeMinSecs: String = "3", + refreshRateRange: Pair<String, String>? = null, + allowInLowPowerMode: String? = null, + minimumHdrPercentOfScreen: String? = null, + sdrHdrRatioMap: List<Pair<String, String>>? = null, +) { + element("highBrightnessMode") { + attribute("", "enabled", enabled) + element("transitionPoint", transitionPoint) + element("minimumLux", minimumLux) + element("timing") { + element("timeWindowSecs", timeWindowSecs) + element("timeMaxSecs", timeMaxSecs) + element("timeMinSecs", timeMinSecs) + } + pair("refreshRate", "minimum", "maximum", refreshRateRange) + element("allowInLowPowerMode", allowInLowPowerMode) + element("minimumHdrPercentOfScreen", minimumHdrPercentOfScreen) + map("sdrHdrRatioMap", "point", "sdrNits", "hdrRatio", sdrHdrRatioMap) + } +} + +fun XmlSerializer.hdrBrightnessConfig( + brightnessMap: List<Pair<String, String>> = listOf(Pair("500", "0.6")), + brightnessIncreaseDebounceMillis: String = "1000", + screenBrightnessRampIncrease: String = "0.02", + brightnessDecreaseDebounceMillis: String = "3000", + screenBrightnessRampDecrease: String = "0.04", + minimumHdrPercentOfScreenForNbm: String? = null, + minimumHdrPercentOfScreenForHbm: String? = null, + allowInLowPowerMode: String? = null, + sdrHdrRatioMap: List<Pair<String, String>>? = null, +) { + element("hdrBrightnessConfig") { + map("brightnessMap", "point", "first", "second", brightnessMap) + element("brightnessIncreaseDebounceMillis", brightnessIncreaseDebounceMillis) + element("screenBrightnessRampIncrease", screenBrightnessRampIncrease) + element("brightnessDecreaseDebounceMillis", brightnessDecreaseDebounceMillis) + element("screenBrightnessRampDecrease", screenBrightnessRampDecrease) + element("minimumHdrPercentOfScreenForNbm", minimumHdrPercentOfScreenForNbm) + element("minimumHdrPercentOfScreenForHbm", minimumHdrPercentOfScreenForHbm) + element("allowInLowPowerMode", allowInLowPowerMode) + map("sdrHdrRatioMap", "point", "first", "second", sdrHdrRatioMap) + } +} + +fun createDisplayConfiguration(content: XmlSerializer.() -> Unit = { }): DisplayConfiguration { + val byteArrayOutputStream = ByteArrayOutputStream() + val xmlSerializer = Xml.newSerializer() + OutputStreamWriter(byteArrayOutputStream).use { writer -> + xmlSerializer.setOutput(writer) + xmlSerializer.startDocument("UTF-8", true) + xmlSerializer.startTag("", "displayConfiguration") + xmlSerializer.content() + xmlSerializer.endTag("", "displayConfiguration") + xmlSerializer.endDocument() + } + return XmlParser.read(ByteArrayInputStream(byteArrayOutputStream.toByteArray())) +} + +private fun XmlSerializer.map( + rootName: String, + nodeName: String, + keyName: String, + valueName: String, + map: List<Pair<String, String>>? +) { + map?.let { m -> + element(rootName) { + m.forEach { e -> pair(nodeName, keyName, valueName, e) } + } + } +} + +private fun XmlSerializer.pair( + nodeName: String, + keyName: String, + valueName: String, + pair: Pair<String, String>? +) { + pair?.let { + element(nodeName) { + element(keyName, pair.first) + element(valueName, pair.second) + } + } +} + +private fun XmlSerializer.element(name: String, content: String?) { + if (content != null) { + startTag("", name) + text(content) + endTag("", name) + } +} + +private fun XmlSerializer.element(name: String, content: XmlSerializer.() -> Unit) { + startTag("", name) + content() + endTag("", name) +}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt b/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt new file mode 100644 index 000000000000..19c6924dfa5b --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt @@ -0,0 +1,158 @@ +/* + * 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.display.config + +import android.util.Spline.createSpline +import androidx.test.filters.SmallTest +import com.android.server.display.DisplayBrightnessState +import com.android.server.display.config.HighBrightnessModeData.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class HdrBrightnessDataTest { + + @Test + fun `test HdrBrightnessData default configuration`() { + val displayConfiguration = createDisplayConfiguration { + hdrBrightnessConfig( + brightnessDecreaseDebounceMillis = "3000", + screenBrightnessRampDecrease = "0.05", + brightnessIncreaseDebounceMillis = "2000", + screenBrightnessRampIncrease = "0.03", + brightnessMap = listOf(Pair("500", "0.6"), Pair("600", "0.7")), + minimumHdrPercentOfScreenForNbm = null, + minimumHdrPercentOfScreenForHbm = null, + allowInLowPowerMode = null, + sdrHdrRatioMap = null, + ) + } + + val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) + assertThat(hdrBrightnessData).isNotNull() + + assertThat(hdrBrightnessData!!.brightnessDecreaseDebounceMillis).isEqualTo(3000) + assertThat(hdrBrightnessData.screenBrightnessRampDecrease).isEqualTo(0.05f) + assertThat(hdrBrightnessData.brightnessIncreaseDebounceMillis).isEqualTo(2000) + assertThat(hdrBrightnessData.screenBrightnessRampIncrease).isEqualTo(0.03f) + + assertThat(hdrBrightnessData.maxBrightnessLimits).hasSize(2) + assertThat(hdrBrightnessData.maxBrightnessLimits).containsEntry(500f, 0.6f) + assertThat(hdrBrightnessData.maxBrightnessLimits).containsEntry(600f, 0.7f) + + assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo( + HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT + ) + assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo( + HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT + ) + assertThat(hdrBrightnessData.allowInLowPowerMode).isFalse() + assertThat(hdrBrightnessData.sdrToHdrRatioSpline).isNull() + } + + @Test + fun `test HdrBrightnessData fallback configuration`() { + val displayConfiguration = createDisplayConfiguration { + hdrBrightnessConfig( + minimumHdrPercentOfScreenForNbm = null, + minimumHdrPercentOfScreenForHbm = null, + allowInLowPowerMode = null, + sdrHdrRatioMap = null, + ) + highBrightnessMode( + minimumHdrPercentOfScreen = "0.2", + sdrHdrRatioMap = listOf(Pair("2.0", "4.0"), Pair("5.0", "8.0")) + ) + } + + val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) + assertThat(hdrBrightnessData).isNotNull() + + assertThat(hdrBrightnessData!!.minimumHdrPercentOfScreenForNbm).isEqualTo(0.2f) + assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.2f) + assertThat(hdrBrightnessData.allowInLowPowerMode).isFalse() + + val expectedSpline = createSpline(floatArrayOf(2.0f, 5.0f), floatArrayOf(4.0f, 8.0f)) + assertThat(hdrBrightnessData.sdrToHdrRatioSpline.toString()) + .isEqualTo(expectedSpline.toString()) + } + + @Test + fun `test HdrBrightnessData fallback configuration no hdrBrightnessConfig`() { + val displayConfiguration = createDisplayConfiguration { + highBrightnessMode( + minimumHdrPercentOfScreen = "0.2", + sdrHdrRatioMap = listOf(Pair("2.0", "4.0"), Pair("5.0", "8.0")) + ) + } + + val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) + assertThat(hdrBrightnessData).isNotNull() + + assertThat(hdrBrightnessData!!.brightnessDecreaseDebounceMillis).isEqualTo(0) + assertThat(hdrBrightnessData.screenBrightnessRampDecrease) + .isEqualTo(DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET) + assertThat(hdrBrightnessData.brightnessIncreaseDebounceMillis).isEqualTo(0) + assertThat(hdrBrightnessData.screenBrightnessRampIncrease) + .isEqualTo(DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET) + + assertThat(hdrBrightnessData.maxBrightnessLimits).hasSize(0) + + assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo(0.2f) + assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.2f) + assertThat(hdrBrightnessData.allowInLowPowerMode).isFalse() + + val expectedSpline = createSpline(floatArrayOf(2.0f, 5.0f), floatArrayOf(4.0f, 8.0f)) + assertThat(hdrBrightnessData.sdrToHdrRatioSpline.toString()) + .isEqualTo(expectedSpline.toString()) + } + + @Test + fun `test HdrBrightnessData configuration no configuration`() { + val displayConfiguration = createDisplayConfiguration() + + val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) + assertThat(hdrBrightnessData).isNull() + } + + @Test + fun `test HdrBrightnessData real configuration`() { + val displayConfiguration = createDisplayConfiguration { + hdrBrightnessConfig( + minimumHdrPercentOfScreenForNbm = "0.3", + minimumHdrPercentOfScreenForHbm = "0.6", + allowInLowPowerMode = "true", + sdrHdrRatioMap = listOf(Pair("3.0", "5.0"), Pair("6.0", "8.0")) + ) + highBrightnessMode( + minimumHdrPercentOfScreen = "0.2", + sdrHdrRatioMap = listOf(Pair("2.0", "4.0"), Pair("5.0", "8.0")) + ) + } + + val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) + assertThat(hdrBrightnessData).isNotNull() + + assertThat(hdrBrightnessData!!.minimumHdrPercentOfScreenForNbm).isEqualTo(0.3f) + assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.6f) + assertThat(hdrBrightnessData.allowInLowPowerMode).isTrue() + + val expectedSpline = createSpline(floatArrayOf(3.0f, 6.0f), floatArrayOf(5.0f, 8.0f)) + assertThat(hdrBrightnessData.sdrToHdrRatioSpline.toString()) + .isEqualTo(expectedSpline.toString()) + } +}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt index 95702aa1bce1..3c77ec925078 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt @@ -25,6 +25,7 @@ import androidx.test.filters.SmallTest import com.android.server.display.DisplayDeviceConfig import com.android.server.display.config.RefreshRateData import com.android.server.display.config.SupportedModeData +import com.android.server.display.config.createRefreshRateData import com.android.server.display.feature.DisplayManagerFlags import com.android.server.display.mode.DisplayModeDirector.DisplayDeviceConfigProvider import com.android.server.testutils.TestHandler diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt index e431c8c3555c..4fc574a77571 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt @@ -29,6 +29,7 @@ import com.android.internal.util.test.FakeSettingsProvider import com.android.server.display.DisplayDeviceConfig import com.android.server.display.config.RefreshRateData import com.android.server.display.config.SupportedModeData +import com.android.server.display.config.createRefreshRateData import com.android.server.display.feature.DisplayManagerFlags import com.android.server.display.mode.DisplayModeDirector.DisplayDeviceConfigProvider import com.android.server.display.mode.SupportedRefreshRatesVote.RefreshRates diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/TestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/TestUtils.kt index 5b07166d63e4..0b34fce18a1b 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/TestUtils.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/TestUtils.kt @@ -16,9 +16,6 @@ package com.android.server.display.mode -import com.android.server.display.config.RefreshRateData -import com.android.server.display.config.SupportedModeData - internal fun createVotesSummary( isDisplayResolutionRangeVotingEnabled: Boolean = true, supportedModesVoteEnabled: Boolean = true, @@ -29,15 +26,3 @@ internal fun createVotesSummary( loggingEnabled, supportsFrameRateOverride) } -fun createRefreshRateData( - defaultRefreshRate: Int = 60, - defaultPeakRefreshRate: Int = 60, - defaultRefreshRateInHbmHdr: Int = 60, - defaultRefreshRateInHbmSunlight: Int = 60, - lowPowerSupportedModes: List<SupportedModeData> = emptyList(), - lowLightBlockingZoneSupportedModes: List<SupportedModeData> = emptyList() -): RefreshRateData { - return RefreshRateData(defaultRefreshRate, defaultPeakRefreshRate, - defaultRefreshRateInHbmHdr, defaultRefreshRateInHbmSunlight, - lowPowerSupportedModes, lowLightBlockingZoneSupportedModes) -} diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java index 419bcb8650a7..e610a32cb549 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -527,7 +527,7 @@ public class ActivityManagerServiceTest { final ProcessRecord appRec = new ProcessRecord(mAms, info, info.processName, uid); final ProcessStatsService tracker = mAms.mProcessStats; - final IApplicationThread appThread = mock(IApplicationThread.class); + final ApplicationThreadDeferred appThread = mock(ApplicationThreadDeferred.class); doReturn(mock(IBinder.class)).when(appThread).asBinder(); appRec.makeActive(appThread, tracker); mAms.mProcessList.addProcessNameLocked(appRec); @@ -701,7 +701,8 @@ public class ActivityManagerServiceTest { final var wpc = fifoProc.getWindowProcessController(); spyOn(wpc); doReturn(true).when(wpc).useFifoUiScheduling(); - fifoProc.makeActive(fifoProc.getThread(), mAms.mProcessStats); + fifoProc.makeActive(new ApplicationThreadDeferred(fifoProc.getThread()), + mAms.mProcessStats); assertTrue(fifoProc.useFifoUiScheduling()); assertTrue(mAms.mSpecifiedFifoProcesses.contains(fifoProc)); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationThreadDeferredTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationThreadDeferredTest.java new file mode 100644 index 000000000000..8f8c1ac51907 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationThreadDeferredTest.java @@ -0,0 +1,99 @@ +/* + * 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.am; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.IApplicationThread; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; + + +/** + * Tests to verify that the ApplicationThreadDeferred properly defers binder calls to paused + * processes. + */ +@SmallTest +public class ApplicationThreadDeferredTest { + private static final String TAG = "ApplicationThreadDeferredTest"; + + private void callDeferredApis(IApplicationThread thread) throws Exception { + thread.clearDnsCache(); + thread.updateHttpProxy(); + thread.updateTimeZone(); + thread.scheduleLowMemory(); + } + + // Verify that the special APIs have been called count times. + private void verifyDeferredApis(IApplicationThread thread, int count) throws Exception { + verify(thread, times(count)).clearDnsCache(); + verify(thread, times(count)).updateHttpProxy(); + verify(thread, times(count)).updateTimeZone(); + verify(thread, times(count)).scheduleLowMemory(); + } + + // Test the baseline behavior of IApplicationThread. If this test fails, all other tests are + // suspect. + @Test + public void testBaseline() throws Exception { + IApplicationThread thread = mock(IApplicationThread.class); + callDeferredApis(thread); + verifyDeferredApis(thread, 1); + } + + // Test the baseline behavior of IApplicationThreadDeferred. If this test fails, all other + // tests are suspect. + @Test + public void testBaselineDeferred() throws Exception { + IApplicationThread thread = mock(ApplicationThreadDeferred.class); + callDeferredApis(thread); + verifyDeferredApis(thread, 1); + } + + // Verify that a deferred thread behaves like a regular thread when it is not paused. + @Test + public void testDeferredUnpaused() throws Exception { + IApplicationThread base = mock(IApplicationThread.class); + ApplicationThreadDeferred thread = new ApplicationThreadDeferred(base, true); + callDeferredApis(thread); + verifyDeferredApis(base, 1); + } + + // Verify that a paused deferred thread thread does not deliver any calls to its parent. Then + // unpause the thread and verify that the collapsed calls are forwarded. + @Test + public void testDeferredPaused() throws Exception { + IApplicationThread base = mock(IApplicationThread.class); + ApplicationThreadDeferred thread = new ApplicationThreadDeferred(base, true); + thread.onProcessPaused(); + callDeferredApis(thread); + callDeferredApis(thread); + verifyDeferredApis(base, 0); + thread.onProcessUnpaused(); + verifyDeferredApis(base, 1); + } + + // TODO: [b/302724778] Remove manual JNI load + static { + System.loadLibrary("mockingservicestestjni"); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java index 80f7a066985b..93066d8d113a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java @@ -189,7 +189,7 @@ public class AsyncProcessStartTest { private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai, boolean wedge) throws Exception { - final IApplicationThread thread = mock(IApplicationThread.class); + final ApplicationThreadDeferred thread = mock(ApplicationThreadDeferred.class); final IBinder threadBinder = new Binder(); doReturn(threadBinder).when(thread).asBinder(); doAnswer((invocation) -> { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 13ba1e5e705c..3aaf2e5c61a6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -325,13 +325,13 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { ProcessRecord.updateProcessRecordNodes(r); mActiveProcesses.add(r); - final IApplicationThread thread; + final ApplicationThreadDeferred thread; if (dead) { - thread = mock(IApplicationThread.class, (invocation) -> { + thread = mock(ApplicationThreadDeferred.class, (invocation) -> { throw new DeadObjectException(); }); } else { - thread = mock(IApplicationThread.class); + thread = mock(ApplicationThreadDeferred.class); } final IBinder threadBinder = new Binder(); doReturn(threadBinder).when(thread).asBinder(); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java index 240ddf51ffdc..0796f419e385 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java @@ -760,7 +760,7 @@ public class CacheOomRankerTest { ProcessStatsService processStatsService = new ProcessStatsService( mock(ActivityManagerService.class), new File(Environment.getDataSystemCeDirectory(), "procstats")); - app.makeActive(mock(IApplicationThread.class), processStatsService); + app.makeActive(mock(ApplicationThreadDeferred.class), processStatsService); return app; } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 6366f24a27e1..1dbd5320cac6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -3209,7 +3209,7 @@ public class MockingOomAdjusterTests { final ProcessReceiverRecord receivers = app.mReceivers; final ProcessProfileRecord profile = app.mProfile; final ProcessProviderRecord providers = app.mProviders; - app.makeActive(mock(IApplicationThread.class), mService.mProcessStats); + app.makeActive(mock(ApplicationThreadDeferred.class), mService.mProcessStats); app.setLastActivityTime(mLastActivityTime); app.setKilledByAm(mKilledByAm); app.setIsolatedEntryPoint(mIsolatedEntryPoint); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java index 89c67d5e32b7..3572d2315a9d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java @@ -193,7 +193,7 @@ public class ProcessObserverTest { private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai) throws Exception { - final IApplicationThread thread = mock(IApplicationThread.class); + final ApplicationThreadDeferred thread = mock(ApplicationThreadDeferred.class); final IBinder threadBinder = new Binder(); doReturn(threadBinder).when(thread).asBinder(); doAnswer((invocation) -> { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java index 5f126774835d..1ff4a27cb787 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java @@ -529,7 +529,7 @@ public final class ServiceBindingOomAdjPolicyTest { @SuppressWarnings("GuardedBy") private ProcessRecord addProcessRecord(int pid, int uid, int procState, int adj, int cap, String packageName) { - final IApplicationThread appThread = mock(IApplicationThread.class); + final ApplicationThreadDeferred appThread = mock(ApplicationThreadDeferred.class); final IBinder threadBinder = mock(IBinder.class); final ProcessRecord app = makeProcessRecord(pid, uid, uid, null, 0, procState, adj, cap, 0L, 0L, packageName, packageName, mAms); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java index 7ec27be0bfc3..3c4363651557 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java @@ -145,7 +145,7 @@ public final class ServiceTimeoutTest { name, // processName name, // packageName mAms); - app.makeActive(mock(IApplicationThread.class), mAms.mProcessStats); + app.makeActive(mock(ApplicationThreadDeferred.class), mAms.mProcessStats); mProcessList.updateLruProcessLocked(app, false, null); final long now = SystemClock.uptimeMillis(); diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java index 396f4da75172..bf7e3a0bd0a6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java @@ -525,7 +525,7 @@ public class TarBackupReaderTest { @Test public void - chooseRestorePolicy_flagOnNotRestoreAnyVersionVToURestoreAndInAllowlist_returnsIgnore() + chooseRestorePolicy_flagOnNotRestoreAnyVersionVToURestoreAndInAllowlist_returnsAccept() throws Exception { mSetFlagsRule.enableFlags( @@ -540,7 +540,8 @@ public class TarBackupReaderTest { FileMetadata info = new FileMetadata(); info.version = Build.VERSION_CODES.UPSIDE_DOWN_CAKE + 1; - PackageInfo packageInfo = createNonRestoreAnyVersionUPackage(); + PackageInfo packageInfo = createNonRestoreAnyVersionPackage( + Build.VERSION_CODES.UPSIDE_DOWN_CAKE); PackageManagerStub.sPackageInfo = packageInfo; doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(FAKE_SIGNATURE_1, @@ -559,7 +560,7 @@ public class TarBackupReaderTest { @Test public void - chooseRestorePolicy_flagOffNotRestoreAnyVersionVToURestoreAndInAllowlist_returnsAccept() + chooseRestorePolicy_flagOffNotRestoreAnyVersionVToURestoreAndInAllowlist_returnsIgnore() throws Exception { mSetFlagsRule.disableFlags( @@ -574,7 +575,8 @@ public class TarBackupReaderTest { FileMetadata info = new FileMetadata(); info.version = Build.VERSION_CODES.UPSIDE_DOWN_CAKE + 1; - PackageInfo packageInfo = createNonRestoreAnyVersionUPackage(); + PackageInfo packageInfo = createNonRestoreAnyVersionPackage( + Build.VERSION_CODES.UPSIDE_DOWN_CAKE); PackageManagerStub.sPackageInfo = packageInfo; doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(FAKE_SIGNATURE_1, @@ -608,7 +610,8 @@ public class TarBackupReaderTest { FileMetadata info = new FileMetadata(); info.version = Build.VERSION_CODES.UPSIDE_DOWN_CAKE + 1; - PackageInfo packageInfo = createNonRestoreAnyVersionUPackage(); + PackageInfo packageInfo = createNonRestoreAnyVersionPackage( + Build.VERSION_CODES.UPSIDE_DOWN_CAKE); PackageManagerStub.sPackageInfo = packageInfo; doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(FAKE_SIGNATURE_1, @@ -716,6 +719,36 @@ public class TarBackupReaderTest { LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER); } + @Test + public void + chooseRestorePolicy_allowlistNotSetNotRestoreAnyVersionVersionMismatch_returnsIgnore() + throws Exception { + mSetFlagsRule.disableFlags( + Flags.FLAG_ENABLE_V_TO_U_RESTORE_FOR_SYSTEM_COMPONENTS_IN_ALLOWLIST); + + TarBackupReader tarBackupReader = createTarBackupReader(); + + Signature[] signatures = new Signature[]{FAKE_SIGNATURE_1}; + FileMetadata info = new FileMetadata(); + info.version = Build.VERSION_CODES.UPSIDE_DOWN_CAKE + 2; + + PackageInfo packageInfo = createNonRestoreAnyVersionPackage( + Build.VERSION_CODES.UPSIDE_DOWN_CAKE + 1); + PackageManagerStub.sPackageInfo = packageInfo; + + doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(FAKE_SIGNATURE_1, + packageInfo.packageName); + RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub, + false /* allowApks */, info, signatures, mMockPackageManagerInternal, + mUserId, mContext); + + assertThat(policy).isEqualTo(RestorePolicy.IGNORE); + ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); + verify(mBackupManagerMonitorMock).onEvent(bundleCaptor.capture()); + assertThat(bundleCaptor.getValue().get(EXTRA_LOG_EVENT_ID)).isEqualTo( + LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER); + } + private TarBackupReader createTarBackupReader() throws Exception { InputStream inputStream = mContext.getResources().openRawResource( R.raw.backup_telephony_no_password); @@ -726,7 +759,7 @@ public class TarBackupReaderTest { return tarBackupReader; } - private PackageInfo createNonRestoreAnyVersionUPackage(){ + private PackageInfo createNonRestoreAnyVersionPackage(int versionCode) { PackageInfo packageInfo = new PackageInfo(); packageInfo.packageName = "test"; packageInfo.applicationInfo = new ApplicationInfo(); @@ -740,7 +773,7 @@ public class TarBackupReaderTest { SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3, null, null)); - packageInfo.versionCode = Build.VERSION_CODES.UPSIDE_DOWN_CAKE; + packageInfo.versionCode = versionCode; return packageInfo; } } diff --git a/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp new file mode 100644 index 000000000000..127d3e8a4136 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp @@ -0,0 +1,58 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_mainline_modularization", +} + +android_test { + name: "CrashRecoveryModuleTests", + + srcs: [ + "*.java", + ], + + static_libs: [ + "androidx.test.core", + "androidx.test.runner", + "mockito-target-extended-minus-junit4", + "services.core", + "truth", + "flag-junit", + ], + + libs: [ + "android.test.mock", + "android.test.base", + "android.test.runner", + ], + + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + + certificate: "platform", + platform_apis: true, + test_suites: [ + "device-tests", + "automotive-tests", + ], +} diff --git a/services/tests/mockingservicestests/src/com/android/server/crashrecovery/AndroidManifest.xml b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/AndroidManifest.xml new file mode 100644 index 000000000000..067f116d003c --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/AndroidManifest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.crashrecovery"> + + <uses-sdk android:targetSdkVersion="35" /> + + <application android:testOnly="true" + android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.crashrecovery" + android:label="Frameworks crashrecovery module test" /> +</manifest> diff --git a/services/tests/mockingservicestests/src/com/android/server/crashrecovery/AndroidTest.xml b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/AndroidTest.xml new file mode 100644 index 000000000000..7b06ebec654d --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/AndroidTest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<configuration description="Runs Crashrecovery Module Tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="CrashRecoveryModuleTests.apk" /> + </target_preparer> + + <option name="test-tag" value="CrashRecoveryModuleTests" /> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.server.crashrecovery" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/services/tests/mockingservicestests/src/com/android/server/crashrecovery/CrashRecoveryModuleTest.java b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/CrashRecoveryModuleTest.java new file mode 100644 index 000000000000..c481f84719f4 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/CrashRecoveryModuleTest.java @@ -0,0 +1,103 @@ +/* + * 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.crashrecovery; + +import static com.android.server.SystemService.PHASE_ACTIVITY_MANAGER_READY; +import static com.android.server.SystemService.PHASE_THIRD_PARTY_APPS_CAN_START; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; + + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.server.PackageWatchdog; +import com.android.server.RescueParty; +import com.android.server.crashrecovery.CrashRecoveryModule.Lifecycle; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +/** + * Test CrashRecoveryModule. + */ +@RunWith(AndroidJUnit4.class) +public class CrashRecoveryModuleTest { + + @Rule + public SetFlagsRule mSetFlagsRule; + + private MockitoSession mSession; + private Lifecycle mLifecycle; + + @Mock PackageWatchdog mPackageWatchdog; + + @Before + public void setup() { + Context context = ApplicationProvider.getApplicationContext(); + mSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .mockStatic(PackageWatchdog.class) + .mockStatic(RescueParty.class) + .startMocking(); + when(PackageWatchdog.getInstance(context)).thenReturn(mPackageWatchdog); + ExtendedMockito.doNothing().when(() -> RescueParty.registerHealthObserver(context)); + mLifecycle = new Lifecycle(context); + } + + @After + public void tearDown() throws Exception { + mSession.finishMocking(); + } + + @Test + public void testLifecycleServiceStart() { + mLifecycle.onStart(); + + verify(mPackageWatchdog, times(1)).noteBoot(); + ExtendedMockito.verify(() -> RescueParty.registerHealthObserver(any()), + Mockito.times(1)); + } + + @Test + public void testLifecycleServiceOnBootPhase() { + doNothing().when(mPackageWatchdog).onPackagesReady(); + + mLifecycle.onBootPhase(PHASE_ACTIVITY_MANAGER_READY); + verify(mPackageWatchdog, never()).onPackagesReady(); + + mLifecycle.onBootPhase(PHASE_THIRD_PARTY_APPS_CAN_START); + verify(mPackageWatchdog, times(1)).onPackagesReady(); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/crashrecovery/OWNERS b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/OWNERS new file mode 100644 index 000000000000..8337fd2453df --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/crashrecovery/OWNERS
\ No newline at end of file diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java index 362607b91763..b13fc530399b 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java @@ -355,7 +355,7 @@ public class WifiPowerStatsCollectorTest { assertThat(stats.getNumBytesRx()).isEqualTo(13321); assertThat(stats.getNumPacketsTx()).isEqualTo(263); assertThat(stats.getNumBytesTx()).isEqualTo(7234); - assertThat(stats.getScanTimeMillis()).isEqualTo(2200); + assertThat(stats.getScanTimeMillis()).isEqualTo(200); assertThat(stats.getRxTimeMillis()).isEqualTo(6000); assertThat(stats.getTxTimeMillis()).isEqualTo(1000); assertThat(stats.getIdleTimeMillis()).isEqualTo(300); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java index ca3055196e6d..62fa95149067 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java @@ -152,6 +152,7 @@ public class AccessibilityUserStateTest { @Test public void onSwitchToAnotherUser_userStateClearedNonDefaultValues() { + String componentNameString = COMPONENT_NAME.flattenToString(); mUserState.getBoundServicesLocked().add(mMockConnection); mUserState.getBindingServicesLocked().add(COMPONENT_NAME); mUserState.setLastSentClientStateLocked( @@ -162,10 +163,13 @@ public class AccessibilityUserStateTest { mUserState.setInteractiveUiTimeoutLocked(30); mUserState.mEnabledServices.add(COMPONENT_NAME); mUserState.mTouchExplorationGrantedServices.add(COMPONENT_NAME); - mUserState.updateShortcutTargetsLocked(Set.of(COMPONENT_NAME.flattenToString()), HARDWARE); - mUserState.updateShortcutTargetsLocked(Set.of(COMPONENT_NAME.flattenToString()), SOFTWARE); - mUserState.updateShortcutTargetsLocked(Set.of(COMPONENT_NAME.flattenToString()), GESTURE); - mUserState.setTargetAssignedToAccessibilityButton(COMPONENT_NAME.flattenToString()); + mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), HARDWARE); + mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), SOFTWARE); + mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), GESTURE); + mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), QUICK_SETTINGS); + mUserState.updateA11yTilesInQsPanelLocked( + Set.of(AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME)); + mUserState.setTargetAssignedToAccessibilityButton(componentNameString); mUserState.setTouchExplorationEnabledLocked(true); mUserState.setMagnificationSingleFingerTripleTapEnabledLocked(true); mUserState.setMagnificationTwoFingerTripleTapEnabledLocked(true); @@ -189,6 +193,8 @@ public class AccessibilityUserStateTest { assertTrue(mUserState.getShortcutTargetsLocked(HARDWARE).isEmpty()); assertTrue(mUserState.getShortcutTargetsLocked(SOFTWARE).isEmpty()); assertTrue(mUserState.getShortcutTargetsLocked(GESTURE).isEmpty()); + assertTrue(mUserState.getShortcutTargetsLocked(QUICK_SETTINGS).isEmpty()); + assertTrue(mUserState.getA11yQsTilesInQsPanel().isEmpty()); assertNull(mUserState.getTargetAssignedToAccessibilityButton()); assertFalse(mUserState.isTouchExplorationEnabledLocked()); assertFalse(mUserState.isMagnificationSingleFingerTripleTapEnabledLocked()); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt index dc8d2390ef2d..0def51691efa 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt +++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt @@ -16,6 +16,8 @@ package com.android.server.accessibility +import android.util.MathUtils.sqrt + import android.companion.virtual.VirtualDeviceManager import android.companion.virtual.VirtualDeviceParams import android.content.Context @@ -59,6 +61,7 @@ class MouseKeysInterceptorTest { companion object { const val DISPLAY_ID = 1 const val DEVICE_ID = 123 + const val MOUSE_POINTER_MOVEMENT_STEP = 1.8f // This delay is required for key events to be sent and handled correctly. // The handler only performs a move/scroll event if it receives the key event // at INTERVAL_MILLIS (which happens in practice). Hence, we need this delay in the tests. @@ -113,8 +116,7 @@ class MouseKeysInterceptorTest { Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager) - mouseKeysInterceptor = MouseKeysInterceptor(mockAms, mockInputManager, - testLooper.looper, DISPLAY_ID) + mouseKeysInterceptor = MouseKeysInterceptor(mockAms, testLooper.looper, DISPLAY_ID) // VirtualMouse is created on a separate thread. // Wait for VirtualMouse to be created before running tests TimeUnit.MILLISECONDS.sleep(20L) @@ -145,7 +147,7 @@ class MouseKeysInterceptorTest { fun whenMouseDirectionalKeyIsPressed_relativeEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS - val keyCode = MouseKeysInterceptor.MouseKeyEvent.DOWN_MOVE.getKeyCodeValue() + val keyCode = MouseKeysInterceptor.MouseKeyEvent.DIAGONAL_DOWN_LEFT_MOVE.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) @@ -153,14 +155,15 @@ class MouseKeysInterceptorTest { testLooper.dispatchAll() // Verify the sendRelativeEvent method is called once and capture the arguments - verifyRelativeEvents(arrayOf<Float>(0f), arrayOf<Float>(1.8f)) + verifyRelativeEvents(arrayOf(-MOUSE_POINTER_MOVEMENT_STEP / sqrt(2.0f)), + arrayOf(MOUSE_POINTER_MOVEMENT_STEP / sqrt(2.0f))) } @Test fun whenClickKeyIsPressed_buttonEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS - val keyCode = MouseKeysInterceptor.MouseKeyEvent.LEFT_CLICK.getKeyCodeValue() + val keyCode = MouseKeysInterceptor.MouseKeyEvent.LEFT_CLICK.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) @@ -179,7 +182,7 @@ class MouseKeysInterceptorTest { @Test fun whenHoldKeyIsPressed_buttonEventIsSent() { val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS - val keyCode = MouseKeysInterceptor.MouseKeyEvent.HOLD.getKeyCodeValue() + val keyCode = MouseKeysInterceptor.MouseKeyEvent.HOLD.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) @@ -195,7 +198,7 @@ class MouseKeysInterceptorTest { @Test fun whenReleaseKeyIsPressed_buttonEventIsSent() { val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS - val keyCode = MouseKeysInterceptor.MouseKeyEvent.RELEASE.getKeyCodeValue() + val keyCode = MouseKeysInterceptor.MouseKeyEvent.RELEASE.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) @@ -209,18 +212,38 @@ class MouseKeysInterceptorTest { } @Test - fun whenScrollUpKeyIsPressed_scrollEventIsSent() { + fun whenScrollToggleOn_ScrollUpKeyIsPressed_scrollEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS - val keyCode = MouseKeysInterceptor.MouseKeyEvent.SCROLL_UP.getKeyCodeValue() + val keyCodeScrollToggle = MouseKeysInterceptor.MouseKeyEvent.SCROLL_TOGGLE.keyCodeValue + val keyCodeScroll = MouseKeysInterceptor.MouseKeyEvent.UP_MOVE_OR_SCROLL.keyCodeValue + + val scrollToggleDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, + keyCodeScrollToggle, 0, 0, DEVICE_ID, 0) + val scrollDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, + keyCodeScroll, 0, 0, DEVICE_ID, 0) + + mouseKeysInterceptor.onKeyEvent(scrollToggleDownEvent, 0) + mouseKeysInterceptor.onKeyEvent(scrollDownEvent, 0) + testLooper.dispatchAll() + + // Verify the sendScrollEvent method is called once and capture the arguments + verifyScrollEvents(arrayOf<Float>(0f), arrayOf<Float>(1.0f)) + } + + @Test + fun whenScrollToggleOff_DirectionalUpKeyIsPressed_RelativeEventIsSent() { + // There should be some delay between the downTime of the key event and calling onKeyEvent + val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS + val keyCode = MouseKeysInterceptor.MouseKeyEvent.UP_MOVE_OR_SCROLL.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) testLooper.dispatchAll() - // Verify the sendScrollEvent method is called once and capture the arguments - verifyScrollEvents(arrayOf<Float>(0f), arrayOf<Float>(1.0f)) + // Verify the sendRelativeEvent method is called once and capture the arguments + verifyRelativeEvents(arrayOf<Float>(0f), arrayOf<Float>(-MOUSE_POINTER_MOVEMENT_STEP)) } private fun verifyRelativeEvents(expectedX: Array<Float>, expectedY: Array<Float>) { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java index 10f4308cbcfb..599a3b86e1af 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java @@ -396,7 +396,7 @@ public class HdmiCecNetworkTest { } @Test - public void cecDevices_tracking_updatesPhysicalAddress() { + public void cecDevices_tracking_updatesPhysicalAddress_add() { int logicalAddress = Constants.ADDR_PLAYBACK_1; int initialPhysicalAddress = 0x1000; int updatedPhysicalAddress = 0x2000; @@ -415,11 +415,12 @@ public class HdmiCecNetworkTest { assertThat(cecDeviceInfo.getPhysicalAddress()).isEqualTo(updatedPhysicalAddress); assertThat(cecDeviceInfo.getDeviceType()).isEqualTo(type); - // ADD for physical address first detected - // UPDATE for updating device with new physical address + // Handle case where PA is changed: Update CEC device information by calling + // addCecDevice(). assertThat(mDeviceEventListenerStatuses).containsExactly( HdmiControlManager.DEVICE_EVENT_ADD_DEVICE, - HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE); + HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE, + HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); } @Test diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index e64397d4223b..316b5faf2a68 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -17,6 +17,7 @@ package com.android.server.media.projection; +import static android.Manifest.permission.RECORD_SENSITIVE_CONTENT; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRIVILEGED; import static android.media.projection.MediaProjectionManager.TYPE_MIRRORING; @@ -50,6 +51,7 @@ import static org.testng.Assert.assertThrows; import android.app.ActivityManagerInternal; import android.app.ActivityOptions.LaunchCookie; +import android.app.KeyguardManager; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; @@ -66,7 +68,9 @@ import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.os.test.TestLooper; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.ContentRecordingSession; import android.view.ContentRecordingSession.RecordContent; @@ -81,6 +85,7 @@ import com.android.server.wm.WindowManagerInternal; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -151,6 +156,9 @@ public class MediaProjectionManagerServiceTest { private ContentRecordingSession mWaitingDisplaySession = createDisplaySession(DEFAULT_DISPLAY); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private ActivityManagerInternal mAmInternal; @Mock @@ -158,6 +166,8 @@ public class MediaProjectionManagerServiceTest { @Mock private PackageManager mPackageManager; @Mock + private KeyguardManager mKeyguardManager; + @Mock private IMediaProjectionWatcherCallback mWatcherCallback; @Mock private MediaProjectionMetricsLogger mMediaProjectionMetricsLogger; @@ -177,6 +187,7 @@ public class MediaProjectionManagerServiceTest { mContext = spy(new ContextWrapper( InstrumentationRegistry.getInstrumentation().getTargetContext())); doReturn(mPackageManager).when(mContext).getPackageManager(); + doReturn(mKeyguardManager).when(mContext).getSystemService(eq(Context.KEYGUARD_SERVICE)); mClock = new OffsettableClock.Stopped(); mWaitingDisplaySession.setWaitingForConsent(true); @@ -246,6 +257,39 @@ public class MediaProjectionManagerServiceTest { assertThat(stoppedCallback2).isFalse(); } + @EnableFlags(android.companion.virtualdevice.flags + .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) + @Test + public void testCreateProjection_keyguardLocked() throws Exception { + MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); + + doReturn(true).when(mKeyguardManager).isKeyguardLocked(); + doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager) + .checkPermission(RECORD_SENSITIVE_CONTENT, projection.packageName); + projection.start(mIMediaProjectionCallback); + projection.notifyVirtualDisplayCreated(10); + + assertThat(mService.getActiveProjectionInfo()).isNull(); + assertThat(mIMediaProjectionCallback.mLatch.await(5, TimeUnit.SECONDS)).isTrue(); + } + + @EnableFlags(android.companion.virtualdevice.flags + .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) + @Test + public void testCreateProjection_keyguardLocked_packageAllowlisted() + throws NameNotFoundException { + MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); + + doReturn(true).when(mKeyguardManager).isKeyguardLocked(); + doReturn(PackageManager.PERMISSION_GRANTED).when(mPackageManager) + .checkPermission(RECORD_SENSITIVE_CONTENT, projection.packageName); + projection.start(mIMediaProjectionCallback); + projection.notifyVirtualDisplayCreated(10); + + // The projection was started because it was allowed to capture the keyguard. + assertThat(mService.getActiveProjectionInfo()).isNotNull(); + } + @Test public void testCreateProjection_attemptReuse_noPriorProjectionGrant() throws NameNotFoundException { @@ -317,6 +361,48 @@ public class MediaProjectionManagerServiceTest { assertThat(secondProjection).isNotEqualTo(projection); } + @EnableFlags(android.companion.virtualdevice.flags + .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) + @Test + public void testKeyguardLocked_stopsActiveProjection() throws Exception { + MediaProjectionManagerService service = + new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector); + MediaProjectionManagerService.MediaProjection projection = + startProjectionPreconditions(service); + projection.start(mIMediaProjectionCallback); + + assertThat(service.getActiveProjectionInfo()).isNotNull(); + + doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager) + .checkPermission(RECORD_SENSITIVE_CONTENT, projection.packageName); + service.onKeyguardLockedStateChanged(true); + + verify(mMediaProjectionMetricsLogger).logStopped(UID, TARGET_UID_UNKNOWN); + assertThat(service.getActiveProjectionInfo()).isNull(); + assertThat(mIMediaProjectionCallback.mLatch.await(5, TimeUnit.SECONDS)).isTrue(); + } + + @EnableFlags(android.companion.virtualdevice.flags + .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) + @Test + public void testKeyguardLocked_packageAllowlisted_doesNotStopActiveProjection() + throws NameNotFoundException { + MediaProjectionManagerService service = + new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector); + MediaProjectionManagerService.MediaProjection projection = + startProjectionPreconditions(service); + projection.start(mIMediaProjectionCallback); + + assertThat(service.getActiveProjectionInfo()).isNotNull(); + + doReturn(PackageManager.PERMISSION_GRANTED).when(mPackageManager).checkPermission( + RECORD_SENSITIVE_CONTENT, projection.packageName); + service.onKeyguardLockedStateChanged(true); + + verifyZeroInteractions(mMediaProjectionMetricsLogger); + assertThat(service.getActiveProjectionInfo()).isNotNull(); + } + @Test public void stop_noActiveProjections_doesNotLog() throws Exception { MediaProjectionManagerService service = diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index 02d3b59fbf87..d714db99f18f 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -1060,6 +1060,23 @@ public final class UserManagerTest { assertThrows(SecurityException.class, userProps::getAlwaysVisible); } + /** + * Test that UserManager.getUserProperties throws the IllegalArgumentException for unsupported + * arguments such as UserHandle.NULL, UserHandle.CURRENT or UserHandle.ALL. + **/ + @MediumTest + @Test + public void testThrowUserPropertiesForUnsupportedUserHandles() throws Exception { + assertThrows(IllegalArgumentException.class, () -> + mUserManager.getUserProperties(UserHandle.of(UserHandle.USER_NULL))); + assertThrows(IllegalArgumentException.class, () -> + mUserManager.getUserProperties(UserHandle.CURRENT)); + assertThrows(IllegalArgumentException.class, () -> + mUserManager.getUserProperties(UserHandle.CURRENT_OR_SELF)); + assertThrows(IllegalArgumentException.class, () -> + mUserManager.getUserProperties(UserHandle.ALL)); + } + // Make sure only max managed profiles can be created @MediumTest @Test @@ -1845,6 +1862,25 @@ public final class UserManagerTest { assertThat(profilesExcludingHidden).asList().doesNotContain(profile.id); } + /** + * Test that UserManager.isQuietModeEnabled return false for unsupported + * arguments such as UserHandle.NULL, UserHandle.CURRENT or UserHandle.ALL. + **/ + @MediumTest + @Test + public void testQuietModeEnabledForUnsupportedUserHandles() throws Exception { + assumeManagedUsersSupported(); + final int mainUserId = mUserManager.getMainUser().getIdentifier(); + UserInfo userInfo = createProfileForUser("Profile", + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); + mUserManager.requestQuietModeEnabled(true, userInfo.getUserHandle()); + assertThat(mUserManager.isQuietModeEnabled(userInfo.getUserHandle())).isTrue(); + assertThat(mUserManager.isQuietModeEnabled(UserHandle.of(UserHandle.USER_NULL))).isFalse(); + assertThat(mUserManager.isQuietModeEnabled(UserHandle.CURRENT)).isFalse(); + assertThat(mUserManager.isQuietModeEnabled(UserHandle.CURRENT_OR_SELF)).isFalse(); + assertThat(mUserManager.isQuietModeEnabled(UserHandle.ALL)).isFalse(); + } + private String generateLongString() { String partialString = "Test Name Test Name Test Name Test Name Test Name Test Name Test " + "Name Test Name Test Name Test Name "; //String of length 100 diff --git a/services/tests/servicestests/src/com/android/server/power/WakefulnessSessionObserverTest.java b/services/tests/servicestests/src/com/android/server/power/WakefulnessSessionObserverTest.java index 698f094c8796..6b32be0b2dfd 100644 --- a/services/tests/servicestests/src/com/android/server/power/WakefulnessSessionObserverTest.java +++ b/services/tests/servicestests/src/com/android/server/power/WakefulnessSessionObserverTest.java @@ -16,9 +16,14 @@ package com.android.server.power; +import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; +import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM; +import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF; import static android.os.PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON; import static android.os.PowerManager.GO_TO_SLEEP_REASON_TIMEOUT; import static android.os.PowerManager.WAKE_REASON_POWER_BUTTON; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.DEFAULT_DISPLAY_GROUP; import static com.android.server.power.ScreenTimeoutOverridePolicy.RELEASE_REASON_UNKNOWN; import static com.android.server.power.ScreenTimeoutOverridePolicy.RELEASE_REASON_USER_ACTIVITY_TOUCH; @@ -27,6 +32,10 @@ import static com.android.server.power.WakefulnessSessionObserver.OVERRIDE_OUTCO import static com.android.server.power.WakefulnessSessionObserver.OVERRIDE_OUTCOME_CANCEL_USER_INTERACTION; import static com.android.server.power.WakefulnessSessionObserver.OVERRIDE_OUTCOME_TIMEOUT_SUCCESS; import static com.android.server.power.WakefulnessSessionObserver.OVERRIDE_OUTCOME_TIMEOUT_USER_INITIATED_REVERT; +import static com.android.server.power.WakefulnessSessionObserver.POLICY_REASON_BRIGHT_INITIATED_REVERT; +import static com.android.server.power.WakefulnessSessionObserver.POLICY_REASON_BRIGHT_UNDIM; +import static com.android.server.power.WakefulnessSessionObserver.POLICY_REASON_OFF_POWER_BUTTON; +import static com.android.server.power.WakefulnessSessionObserver.POLICY_REASON_OFF_TIMEOUT; import static com.google.common.truth.Truth.assertThat; @@ -40,18 +49,24 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.ContextWrapper; import android.content.res.Resources; +import android.hardware.display.DisplayManagerInternal; +import android.os.Handler; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.UserHandle; import android.provider.Settings; import android.test.mock.MockContentResolver; +import android.view.DisplayAddress; +import android.view.DisplayInfo; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.R; import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.testutils.OffsettableClock; +import com.android.server.testutils.TestHandler; import org.junit.After; import org.junit.Before; @@ -65,25 +80,18 @@ import org.mockito.MockitoAnnotations; public class WakefulnessSessionObserverTest { private static final int DEFAULT_SCREEN_OFF_TIMEOUT_MS = 30000; private static final int OVERRIDE_SCREEN_OFF_TIMEOUT_MS = 15000; + private static final int DISPLAY_PORT = 0xFF; + private static final long DISPLAY_MODEL = 0xEEEEEEEEL; private WakefulnessSessionObserver mWakefulnessSessionObserver; private Context mContext; private OffsettableClock mTestClock; @Mock private WakefulnessSessionObserver.WakefulnessSessionFrameworkStatsLogger mWakefulnessSessionFrameworkStatsLogger; - private WakefulnessSessionObserver.Injector mInjector = - new WakefulnessSessionObserver.Injector() { - @Override - WakefulnessSessionObserver.WakefulnessSessionFrameworkStatsLogger - getWakefulnessSessionFrameworkStatsLogger() { - return mWakefulnessSessionFrameworkStatsLogger; - } - @Override - WakefulnessSessionObserver.Clock getClock() { - return mTestClock::now; - } - }; + @Mock + private DisplayManagerInternal mDisplayManagerInternal; + private TestHandler mHandler; @Before public void setUp() { mTestClock = new OffsettableClock.Stopped(); @@ -95,7 +103,7 @@ public class WakefulnessSessionObserverTest { final Resources res = spy(mContext.getResources()); doReturn(OVERRIDE_SCREEN_OFF_TIMEOUT_MS).when(res).getInteger( - com.android.internal.R.integer.config_screenTimeoutOverride); + R.integer.config_screenTimeoutOverride); when(mContext.getResources()).thenReturn(res); FakeSettingsProvider.clearSettingsProvider(); MockContentResolver mockContentResolver = new MockContentResolver(); @@ -104,7 +112,32 @@ public class WakefulnessSessionObserverTest { Settings.System.putIntForUser(mockContentResolver, Settings.System.SCREEN_OFF_TIMEOUT, DEFAULT_SCREEN_OFF_TIMEOUT_MS, UserHandle.USER_CURRENT); - mWakefulnessSessionObserver = new WakefulnessSessionObserver(mContext, mInjector); + final DisplayInfo info = new DisplayInfo(); + info.address = DisplayAddress.fromPortAndModel(DISPLAY_PORT, DISPLAY_MODEL); + mHandler = new TestHandler(null); + mWakefulnessSessionObserver = new WakefulnessSessionObserver( + mContext, new WakefulnessSessionObserver.Injector() { + @Override + WakefulnessSessionObserver.WakefulnessSessionFrameworkStatsLogger + getWakefulnessSessionFrameworkStatsLogger() { + return mWakefulnessSessionFrameworkStatsLogger; + } + @Override + WakefulnessSessionObserver.Clock getClock() { + return mTestClock::now; + } + @Override + Handler getHandler() { + return mHandler; + } + @Override + DisplayManagerInternal getDisplayManagerInternal() { + when(mDisplayManagerInternal.getDisplayInfo(DEFAULT_DISPLAY)) + .thenReturn(info); + return mDisplayManagerInternal; + } + } + ); } @After @@ -317,6 +350,167 @@ public class WakefulnessSessionObserverTest { DEFAULT_SCREEN_OFF_TIMEOUT_MS); // default timeout ms } + @Test + public void testOnScreenPolicyUpdate_OffByTimeout() { + int userActivity = PowerManager.USER_ACTIVITY_EVENT_ATTENTION; + long userActivityTimestamp = mTestClock.now(); + mWakefulnessSessionObserver.notifyUserActivity( + userActivityTimestamp, DEFAULT_DISPLAY_GROUP, userActivity); + mWakefulnessSessionObserver.onScreenPolicyUpdate( + mTestClock.now(), DEFAULT_DISPLAY_GROUP, POLICY_DIM); + mWakefulnessSessionObserver.onWakefulnessChangeStarted( + DEFAULT_DISPLAY_GROUP, PowerManagerInternal.WAKEFULNESS_AWAKE, + WAKE_REASON_POWER_BUTTON, mTestClock.now()); + int advancedTime = 5; + advanceTime(advancedTime); + mWakefulnessSessionObserver.onScreenPolicyUpdate(mTestClock.now(), DEFAULT_DISPLAY_GROUP, + POLICY_OFF); + mWakefulnessSessionObserver.onWakefulnessChangeStarted( + DEFAULT_DISPLAY_GROUP, PowerManagerInternal.WAKEFULNESS_ASLEEP, + GO_TO_SLEEP_REASON_TIMEOUT, mTestClock.now()); + + verify(mWakefulnessSessionFrameworkStatsLogger) + .logDimEvent( + DISPLAY_PORT, // physical display port id + POLICY_REASON_OFF_TIMEOUT, // policy reason + userActivity, // last user activity event + advancedTime, // last user activity timestamp + advancedTime, // dim duration ms + DEFAULT_SCREEN_OFF_TIMEOUT_MS); // default Timeout Ms + } + + @Test + public void testOnScreenPolicyUpdate_NoLogging_NotDefaultDisplayGroup() { + int powerGroupId = 1; + int userActivity = PowerManager.USER_ACTIVITY_EVENT_ATTENTION; + long userActivityTimestamp = mTestClock.now(); + int advancedTime = 5; + mWakefulnessSessionObserver.notifyUserActivity( + userActivityTimestamp, powerGroupId, userActivity); + mWakefulnessSessionObserver.onScreenPolicyUpdate( + mTestClock.now(), powerGroupId, POLICY_DIM); + advanceTime(advancedTime); + mWakefulnessSessionObserver.onScreenPolicyUpdate(mTestClock.now(), powerGroupId, + POLICY_OFF); + + verify(mWakefulnessSessionFrameworkStatsLogger, never()) + .logDimEvent( + DISPLAY_PORT, // physical display port id + POLICY_REASON_OFF_TIMEOUT, // policy reason + userActivity, // last user activity event + advancedTime, // last user activity timestamp + advancedTime, // dim duration ms + DEFAULT_SCREEN_OFF_TIMEOUT_MS); // default Timeout Ms + } + + @Test + public void testOnScreenPolicyUpdate_OffByPowerButton() { + // ----- initialize start ----- + mWakefulnessSessionObserver.onWakefulnessChangeStarted( + DEFAULT_DISPLAY_GROUP, PowerManagerInternal.WAKEFULNESS_AWAKE, + WAKE_REASON_POWER_BUTTON, mTestClock.now()); + + int userActivity = PowerManager.USER_ACTIVITY_EVENT_ACCESSIBILITY; + long userActivityTimestamp = mTestClock.now(); + mWakefulnessSessionObserver.notifyUserActivity( + userActivityTimestamp, DEFAULT_DISPLAY_GROUP, userActivity); + mWakefulnessSessionObserver.onScreenPolicyUpdate( + mTestClock.now(), DEFAULT_DISPLAY_GROUP, POLICY_DIM); + // ----- initialize end ----- + + int dimDuration = 500; + advanceTime(dimDuration); + int userActivityDuration = dimDuration; + mWakefulnessSessionObserver.notifyUserActivity( + mTestClock.now(), DEFAULT_DISPLAY_GROUP, PowerManager.USER_ACTIVITY_EVENT_BUTTON); + mWakefulnessSessionObserver.onWakefulnessChangeStarted( + DEFAULT_DISPLAY_GROUP, PowerManagerInternal.WAKEFULNESS_ASLEEP, + GO_TO_SLEEP_REASON_POWER_BUTTON, mTestClock.now()); + + verify(mWakefulnessSessionFrameworkStatsLogger) + .logDimEvent( + DISPLAY_PORT, // physical display port id + POLICY_REASON_OFF_POWER_BUTTON, // policy reason + userActivity, // last user activity event + userActivityDuration, // last user activity timestamp + dimDuration, // dim duration ms + DEFAULT_SCREEN_OFF_TIMEOUT_MS); // default Timeout Ms + assertThat(mHandler.getPendingMessages()).isEmpty(); + } + + @Test + public void testOnScreenPolicyUpdate_Undim() { + // ----- initialize start ----- + int userActivity = PowerManager.USER_ACTIVITY_EVENT_TOUCH; + long userActivityTimestamp = mTestClock.now(); + mWakefulnessSessionObserver.notifyUserActivity( + userActivityTimestamp, DEFAULT_DISPLAY_GROUP, userActivity); + mWakefulnessSessionObserver.onScreenPolicyUpdate( + mTestClock.now(), DEFAULT_DISPLAY_GROUP, POLICY_DIM); + mWakefulnessSessionObserver.mPowerGroups.get(DEFAULT_DISPLAY_GROUP).mIsInteractive = true; + // ----- initialize end ----- + + int dimDurationMs = 5; + advanceTime(dimDurationMs); + mWakefulnessSessionObserver.onScreenPolicyUpdate( + mTestClock.now(), DEFAULT_DISPLAY_GROUP, POLICY_BRIGHT); + + int expectedLastUserActivityTimeMs = (int) (mTestClock.now() - userActivityTimestamp); + + mHandler.flush(); + verify(mWakefulnessSessionFrameworkStatsLogger) + .logDimEvent( + DISPLAY_PORT, // physical display port id + POLICY_REASON_BRIGHT_UNDIM, // policy reason + userActivity, // last user activity event + expectedLastUserActivityTimeMs, // last user activity timestamp + dimDurationMs, // dim duration ms + DEFAULT_SCREEN_OFF_TIMEOUT_MS); // default Timeout Ms + } + + @Test + public void testOnScreenPolicyUpdate_BrightInitiatedRevert() { + // ----- initialize start ----- + mWakefulnessSessionObserver.onScreenPolicyUpdate( + mTestClock.now(), DEFAULT_DISPLAY_GROUP, POLICY_DIM); + int dimDurationMs = 500; + advanceTime(dimDurationMs); + int userActivity = PowerManager.USER_ACTIVITY_EVENT_BUTTON; + long userActivityTimestamp = mTestClock.now(); + mWakefulnessSessionObserver.notifyUserActivity( + userActivityTimestamp, DEFAULT_DISPLAY_GROUP, userActivity); + int userActivityTime = 5; + advanceTime(userActivityTime); + dimDurationMs += userActivityTime; + mWakefulnessSessionObserver.onScreenPolicyUpdate( + mTestClock.now(), DEFAULT_DISPLAY_GROUP, POLICY_OFF); + mWakefulnessSessionObserver.onWakefulnessChangeStarted( + DEFAULT_DISPLAY_GROUP, PowerManagerInternal.WAKEFULNESS_ASLEEP, + GO_TO_SLEEP_REASON_POWER_BUTTON, mTestClock.now()); + + mWakefulnessSessionObserver.mPowerGroups.get(DEFAULT_DISPLAY_GROUP) + .mPastDimDurationMs = dimDurationMs; + // ----- initialize end ----- + + int advancedTime = 5; + advanceTime(advancedTime); // shorter than 5000 ms + userActivityTime += advancedTime; + mWakefulnessSessionObserver.onScreenPolicyUpdate(mTestClock.now(), DEFAULT_DISPLAY_GROUP, + POLICY_BRIGHT); + mWakefulnessSessionObserver.onWakefulnessChangeStarted( + DEFAULT_DISPLAY_GROUP, PowerManagerInternal.WAKEFULNESS_AWAKE, + WAKE_REASON_POWER_BUTTON, mTestClock.now()); + + verify(mWakefulnessSessionFrameworkStatsLogger) + .logDimEvent( + DISPLAY_PORT, // physical display port id + POLICY_REASON_BRIGHT_INITIATED_REVERT, // policy reason + userActivity, // last user activity event + userActivityTime, // last user activity timestamp + dimDurationMs, // dim duration ms + DEFAULT_SCREEN_OFF_TIMEOUT_MS); // default Timeout Ms + } + private void advanceTime(long timeMs) { mTestClock.fastForward(timeMs); } diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java index bf478162c46e..1decd36be394 100644 --- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java @@ -151,7 +151,6 @@ public class HintManagerServiceTest { private HintManagerService mService; private ChannelConfig mConfig; - private ApplicationInfo mApplicationInfo; private static Answer<Long> fakeCreateWithConfig(Long ptr, Long sessionId) { return new Answer<Long>() { @@ -168,12 +167,12 @@ public class HintManagerServiceTest { mConfig = new ChannelConfig(); mConfig.readFlagBitmask = 1; mConfig.writeFlagBitmask = 2; - mApplicationInfo = new ApplicationInfo(); - mApplicationInfo.category = ApplicationInfo.CATEGORY_GAME; + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.category = ApplicationInfo.CATEGORY_GAME; when(mContext.getPackageManager()).thenReturn(mMockPackageManager); when(mMockPackageManager.getNameForUid(anyInt())).thenReturn(TEST_APP_NAME); when(mMockPackageManager.getApplicationInfo(eq(TEST_APP_NAME), anyInt())) - .thenReturn(mApplicationInfo); + .thenReturn(applicationInfo); when(mNativeWrapperMock.halGetHintSessionPreferredRate()) .thenReturn(DEFAULT_HINT_PREFERRED_RATE); when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A), diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index 28a5db904bd9..e06d939a34f7 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -223,6 +223,10 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { Settings.System.putInt(getContext().getContentResolver(), Settings.System.NOTIFICATION_LIGHT_PULSE, 1); + // Enable notification cooldown independent of device Settings + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ENABLED, 1); + Resources resources = spy(getContext().getResources()); when(resources.getBoolean(R.bool.config_useAttentionLight)).thenReturn(true); when(resources.getBoolean( diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java index ff1308c4f6db..1c8cb8f90f1e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java @@ -137,4 +137,9 @@ public class ZenModeEventLoggerFake extends ZenModeEventLogger { checkInRange(i); return mChanges.get(i).getActiveRuleTypes(); } + + public int getChangeOrigin(int i) throws IllegalArgumentException { + checkInRange(i); + return mChanges.get(i).getChangeOrigin(); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 7bb633e6e1e0..4bbbc2b28dad 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -818,7 +818,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // 1. Current ringer is normal when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL); // Set zen to priority-only with all notification sounds muted (so ringer will be muted) - Policy totalSilence = new Policy(0,0,0); + Policy totalSilence = new Policy(0, 0, 0); mZenModeHelper.setNotificationPolicy(totalSilence, UPDATE_ORIGIN_APP, 1); mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; @@ -873,7 +873,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // even when ringer is muted (since all ringer sounds cannot bypass DND), // system stream is still affected by ringer mode - mZenModeHelper.setNotificationPolicy(new Policy(0,0,0), UPDATE_ORIGIN_APP, 1); + mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0), UPDATE_ORIGIN_APP, 1); mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY, UPDATE_ORIGIN_APP, "test", "caller", 1); ZenModeHelper.RingerModeDelegate ringerModeDelegateRingerNotMuted = @@ -1065,9 +1065,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testParcelConfig() { mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_EVENTS - | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS - | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, - PRIORITY_SENDERS_STARRED, 0, CONVERSATION_SENDERS_ANYONE), UPDATE_ORIGIN_UNKNOWN, + | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS + | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, + PRIORITY_SENDERS_STARRED, 0, CONVERSATION_SENDERS_ANYONE), + UPDATE_ORIGIN_UNKNOWN, 1); mZenModeHelper.setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) @@ -1085,13 +1086,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testWriteXml() throws Exception { mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_EVENTS - | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS - | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, - PRIORITY_SENDERS_STARRED, SUPPRESSED_EFFECT_BADGE, CONVERSATION_SENDERS_ANYONE), + | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS + | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, + PRIORITY_SENDERS_STARRED, SUPPRESSED_EFFECT_BADGE, + CONVERSATION_SENDERS_ANYONE), UPDATE_ORIGIN_UNKNOWN, 1); mZenModeHelper.setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder() - .setShouldDimWallpaper(true) - .setShouldDisplayGrayscale(true) + .setShouldDimWallpaper(true) + .setShouldDisplayGrayscale(true) .build(), UPDATE_ORIGIN_UNKNOWN, "test", 1); mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY, UPDATE_ORIGIN_UNKNOWN, "test", "me", 1); @@ -2210,7 +2212,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { customDefaultRule.name = "Schedule Default Rule"; customDefaultRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; ScheduleInfo scheduleInfo = new ScheduleInfo(); - scheduleInfo.days = new int[] { Calendar.SUNDAY }; + scheduleInfo.days = new int[]{Calendar.SUNDAY}; scheduleInfo.startHour = 18; scheduleInfo.endHour = 19; customDefaultRule.conditionId = ZenModeConfig.toScheduleConditionId(scheduleInfo); @@ -3027,7 +3029,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Turn zen mode on (to important_interruptions) // Need to additionally call the looper in order to finish the post-apply-config process mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - Flags.modesApi() ? UPDATE_ORIGIN_USER: UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, + Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID); // Now turn zen mode off, but via a different package UID -- this should get registered as @@ -3060,6 +3062,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertTrue(mZenModeEventLogger.getIsUserAction(0)); assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0)); checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); + // change origin should be populated only under modes_ui + assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo( + (Flags.modesApi() && Flags.modesUi()) ? UPDATE_ORIGIN_USER : 0); // and from turning zen mode off: // - event ID: DND_TURNED_OFF @@ -3082,6 +3087,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { } else { checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); } + assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo( + Flags.modesUi() ? UPDATE_ORIGIN_APP : 0); } @Test @@ -3098,17 +3105,21 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID); // Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE + // Note that pre-modes_ui, this event serves as a test that automatic changes to an app's + // that look like they're coming from the system are attributed to the app, but when + // modes_ui is true, we opt to trust the provided change origin. mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + Flags.modesUi() ? UPDATE_ORIGIN_APP : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, + CUSTOM_PKG_UID); // Event 2: "User" turns off the automatic rule (sets it to not enabled) zenRule.setEnabled(false); mZenModeHelper.updateAutomaticZenRule(id, zenRule, - Flags.modesApi() ? UPDATE_ORIGIN_USER: UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", + Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", Process.SYSTEM_UID); AutomaticZenRule systemRule = new AutomaticZenRule("systemRule", @@ -3118,7 +3129,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String systemId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), systemRule, - Flags.modesApi() ? UPDATE_ORIGIN_USER: UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", + Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); // Event 3: turn on the system rule @@ -3128,7 +3139,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Event 4: "User" deletes the rule mZenModeHelper.removeAutomaticZenRule(systemId, - Flags.modesApi() ? UPDATE_ORIGIN_USER: UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", + Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", Process.SYSTEM_UID); // In total, this represents 4 events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); @@ -3151,9 +3162,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertFalse(mZenModeEventLogger.getIsUserAction(0)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0)); + assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo( + Flags.modesUi() ? UPDATE_ORIGIN_APP : 0); // When the automatic rule is disabled, this should turn off zen mode and also count as a // user action. We don't care what the consolidated policy is when DND turns off. + // When modes_ui is true, this event should look like a user action attributed to the + // specific app. assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), mZenModeEventLogger.getEventId(1)); assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1)); @@ -3161,12 +3176,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(1)); assertEquals(0, mZenModeEventLogger.getNumRulesActive(1)); assertTrue(mZenModeEventLogger.getIsUserAction(1)); - assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); + assertThat(mZenModeEventLogger.getPackageUid(1)).isEqualTo( + Flags.modesUi() ? CUSTOM_PKG_UID : Process.SYSTEM_UID); if (Flags.modesApi()) { assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull(); } else { checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); } + assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo( + Flags.modesUi() ? UPDATE_ORIGIN_USER : 0); // When the system rule is enabled, this counts as an automatic action that comes from the // system and turns on DND @@ -3176,6 +3194,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(1, mZenModeEventLogger.getNumRulesActive(2)); assertFalse(mZenModeEventLogger.getIsUserAction(2)); assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(2)); + assertThat(mZenModeEventLogger.getChangeOrigin(2)).isEqualTo( + Flags.modesUi() ? UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI : 0); // When the system rule is deleted, we consider this a user action that turns DND off // (again) @@ -3185,6 +3205,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(0, mZenModeEventLogger.getNumRulesActive(3)); assertTrue(mZenModeEventLogger.getIsUserAction(3)); assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(3)); + assertThat(mZenModeEventLogger.getChangeOrigin(3)).isEqualTo( + Flags.modesUi() ? UPDATE_ORIGIN_USER : 0); } @Test @@ -3238,6 +3260,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); assertTrue(mZenModeEventLogger.getIsUserAction(0)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); + assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo( + Flags.modesUi() ? UPDATE_ORIGIN_USER : 0); // Automatic rule turned off automatically by app: // - event ID: DND_TURNED_OFF @@ -3249,6 +3273,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(0, mZenModeEventLogger.getNumRulesActive(1)); assertFalse(mZenModeEventLogger.getIsUserAction(1)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1)); + assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo( + Flags.modesUi() ? UPDATE_ORIGIN_APP : 0); // Automatic rule turned on automatically by app: // - event ID: DND_TURNED_ON @@ -3261,6 +3287,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(1, mZenModeEventLogger.getNumRulesActive(2)); assertFalse(mZenModeEventLogger.getIsUserAction(2)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(2)); + assertThat(mZenModeEventLogger.getChangeOrigin(2)).isEqualTo( + Flags.modesUi() ? UPDATE_ORIGIN_APP : 0); // Automatic rule turned off automatically by the user: // - event ID: DND_TURNED_ON @@ -3272,6 +3300,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(0, mZenModeEventLogger.getNumRulesActive(3)); assertTrue(mZenModeEventLogger.getIsUserAction(3)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(3)); + assertThat(mZenModeEventLogger.getChangeOrigin(3)).isEqualTo( + Flags.modesUi() ? UPDATE_ORIGIN_USER : 0); } @Test @@ -3335,7 +3365,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule zenRule = new AutomaticZenRule("name", null, - new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + new ComponentName("android", "ScheduleConditionProvider"), ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); @@ -3345,7 +3375,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Rule 2, same as rule 1 AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", null, - new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + new ComponentName("android", "ScheduleConditionProvider"), ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); @@ -3395,7 +3425,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0)); assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); assertFalse(mZenModeEventLogger.getIsUserAction(0)); - assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); + assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0)); checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0)); // Event 2: rule 2 turns on. This should not change anything about the policy, so the only @@ -3404,7 +3434,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeEventLogger.getEventId(1)); assertEquals(2, mZenModeEventLogger.getNumRulesActive(1)); assertFalse(mZenModeEventLogger.getIsUserAction(1)); - assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1)); + assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(1)); // Event 3: rule 3 turns on. This should trigger a policy change, and be classified as such, @@ -3482,9 +3512,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Turn on rule 1; call looks like it's from the system. Because setting a condition is // typically an automatic (non-user-initiated) action, expect the calling UID to be // re-evaluated to the one associated with CUSTOM_PKG_NAME. + // When modes_ui is true: we expect the change origin to be the source of truth. mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + Flags.modesUi() ? UPDATE_ORIGIN_APP : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, + Process.SYSTEM_UID); // Second: turn on rule 2. This is a system-owned rule and the UID should not be modified // (nor even looked up; the mock PackageManager won't handle "android" as input). @@ -3493,7 +3525,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // Disable rule 1. Because this looks like a user action, the UID should not be modified - // from the system-provided one. + // from the system-provided one unless modes_ui is true. zenRule.setEnabled(false); mZenModeHelper.updateAutomaticZenRule(id, zenRule, UPDATE_ORIGIN_USER, "", Process.SYSTEM_UID); @@ -3504,6 +3536,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Change rule 2's condition, but from some other UID. Since it doesn't look like it's from // the system, we keep the UID info. + // Note that this probably shouldn't be able to occur in real scenarios. mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_FALSE), UPDATE_ORIGIN_APP, 12345); @@ -3528,11 +3561,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); // Third event: disable rule 1. This looks like a user action so UID should be left alone. + // When modes_ui is true, we assign log this user action with the app that owns the rule. assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId(), mZenModeEventLogger.getEventId(2)); assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(2)); assertTrue(mZenModeEventLogger.getIsUserAction(2)); - assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(2)); + assertThat(mZenModeEventLogger.getPackageUid(2)).isEqualTo( + Flags.modesUi() ? CUSTOM_PKG_UID : Process.SYSTEM_UID); // Fourth event: turns on manual mode. Doesn't change effective policy so this is just a // change in active rules. Confirm that the package UID is left unchanged. @@ -6202,7 +6237,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void setManualZenRuleDeviceEffects_noPreexistingMode() { ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) - .build(); + .build(); mZenModeHelper.setManualZenRuleDeviceEffects(effects, UPDATE_ORIGIN_USER, "settings", 1000); assertThat(mZenModeHelper.getConfig().manualRule).isNotNull(); @@ -6339,21 +6374,21 @@ public class ZenModeHelperTest extends UiServiceTestCase { private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA = Correspondence.transforming(zr -> { - Parcel p = Parcel.obtain(); - try { - zr.writeToParcel(p, 0); - p.setDataPosition(0); - ZenRule copy = new ZenRule(p); - copy.creationTime = 0; - copy.userModifiedFields = 0; - copy.zenPolicyUserModifiedFields = 0; - copy.zenDeviceEffectsUserModifiedFields = 0; - return copy; - } finally { - p.recycle(); - } - }, - "Ignoring timestamp and userModifiedFields"); + Parcel p = Parcel.obtain(); + try { + zr.writeToParcel(p, 0); + p.setDataPosition(0); + ZenRule copy = new ZenRule(p); + copy.creationTime = 0; + copy.userModifiedFields = 0; + copy.zenPolicyUserModifiedFields = 0; + copy.zenDeviceEffectsUserModifiedFields = 0; + return copy; + } finally { + p.recycle(); + } + }, + "Ignoring timestamp and userModifiedFields"); private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy, @Nullable Boolean conditionActive) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index eb8825c0c08b..b4505fad1b20 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -684,7 +684,8 @@ public class ActivityRecordTests extends WindowTestsBase { // Asserts fixed orientation request is not ignored, and the orientation is changed. assertNotEquals(activityCurOrientation, activity.getConfiguration().orientation); - assertTrue(activity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); } @Test @@ -717,7 +718,8 @@ public class ActivityRecordTests extends WindowTestsBase { // Relaunching the app should still respect the orientation request. assertEquals(ORIENTATION_PORTRAIT, activity.getConfiguration().orientation); - assertTrue(activity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java index 433b0913cd6d..867f01fd4699 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -143,8 +143,8 @@ class AppCompatActivityRobot { } void setLetterboxedForFixedOrientationAndAspectRatio(boolean enabled) { - doReturn(enabled).when(mActivityStack.top()) - .isLetterboxedForFixedOrientationAndAspectRatio(); + doReturn(enabled).when(mActivityStack.top().mAppCompatController + .getAppCompatAspectRatioPolicy()).isLetterboxedForFixedOrientationAndAspectRatio(); } void enableTreatmentForTopActivity(boolean enabled) { @@ -403,6 +403,7 @@ class AppCompatActivityRobot { spyOn(activity); spyOn(activity.mAppCompatController.getTransparentPolicy()); spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides()); + spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy()); spyOn(activity.mLetterboxUiController); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java index 0c1fbf3cb3d7..af4394acce67 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java @@ -25,15 +25,11 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_OPEN; import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE; import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -41,7 +37,6 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.server.wm.WindowContainer.POSITION_BOTTOM; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -51,14 +46,11 @@ import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.annotation.Nullable; -import android.graphics.Rect; -import android.gui.DropInputMode; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; @@ -841,353 +833,6 @@ public class AppTransitionControllerTest extends WindowTestsBase { } @Test - public void testOverrideTaskFragmentAdapter_overrideWithEmbeddedActivity() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord activity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(activity); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // Animation run by the remote handler. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_noOverrideWithOnlyTaskFragmentFillingTask() { - final Task task = createTask(mDisplayContent); - final ActivityRecord closingActivity = createActivityRecord(task); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - - // Make sure the TaskFragment is not embedded. - assertFalse(taskFragment.isEmbeddedWithBoundsOverride()); - final ActivityRecord openingActivity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(closingActivity); - prepareActivityForAppTransition(openingActivity); - final int uid = 12345; - closingActivity.info.applicationInfo.uid = uid; - openingActivity.info.applicationInfo.uid = uid; - task.effectiveUid = uid; - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, - null /* changingTaskFragment */); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // Animation is not run by the remote handler because the activity is filling the Task. - assertFalse(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_overrideWithTaskFragmentNotFillingTask() { - final Task task = createTask(mDisplayContent); - final ActivityRecord closingActivity = createActivityRecord(task); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - - // Make sure the TaskFragment is embedded. - taskFragment.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); - final Rect embeddedBounds = new Rect(task.getBounds()); - embeddedBounds.right = embeddedBounds.left + embeddedBounds.width() / 2; - taskFragment.setBounds(embeddedBounds); - assertTrue(taskFragment.isEmbeddedWithBoundsOverride()); - final ActivityRecord openingActivity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(closingActivity); - prepareActivityForAppTransition(openingActivity); - final int uid = 12345; - closingActivity.info.applicationInfo.uid = uid; - openingActivity.info.applicationInfo.uid = uid; - task.effectiveUid = uid; - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, - null /* changingTaskFragment */); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // Animation run by the remote handler. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_overrideWithNonEmbeddedActivity() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Closing non-embedded activity. - final ActivityRecord closingActivity = createActivityRecord(task); - prepareActivityForAppTransition(closingActivity); - // Opening TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord openingActivity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(openingActivity); - task.effectiveUid = openingActivity.getUid(); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // Animation run by the remote handler. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_overrideEmbeddedActivityWithDiffUid() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Closing TaskFragment with embedded activity. - final TaskFragment taskFragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord closingActivity = taskFragment1.getTopMostActivity(); - prepareActivityForAppTransition(closingActivity); - closingActivity.info.applicationInfo.uid = 12345; - // Opening TaskFragment with embedded activity with different UID. - final TaskFragment taskFragment2 = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord openingActivity = taskFragment2.getTopMostActivity(); - prepareActivityForAppTransition(openingActivity); - openingActivity.info.applicationInfo.uid = 54321; - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment1); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // Animation run by the remote handler. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_noOverrideWithTwoApps() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Closing activity in Task1. - final ActivityRecord closingActivity = createActivityRecord(mDisplayContent); - prepareActivityForAppTransition(closingActivity); - // Opening TaskFragment with embedded activity in Task2. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord openingActivity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(openingActivity); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // Animation not run by the remote handler. - assertFalse(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_noOverrideNonEmbeddedActivityWithDiffUid() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Closing TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord closingActivity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(closingActivity); - closingActivity.info.applicationInfo.uid = 12345; - task.effectiveUid = closingActivity.getUid(); - // Opening non-embedded activity with different UID. - final ActivityRecord openingActivity = createActivityRecord(task); - prepareActivityForAppTransition(openingActivity); - openingActivity.info.applicationInfo.uid = 54321; - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // Animation should not run by the remote handler when there are non-embedded activities of - // different UID. - assertFalse(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_noOverrideWithWallpaper() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord activity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(activity); - // Set wallpaper as visible. - final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, - mock(IBinder.class), true, mDisplayContent, true /* ownerCanManageAppTokens */); - spyOn(mDisplayContent.mWallpaperController); - doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible(); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // Animation should not run by the remote handler when there is wallpaper in the transition. - assertFalse(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_inputProtectedForUntrustedAnimation() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with embedded activities, one is trusted embedded, and the other - // one is untrusted embedded. - final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .createActivityCount(2) - .setOrganizer(organizer) - .build(); - final ActivityRecord activity0 = taskFragment.getChildAt(0).asActivityRecord(); - final ActivityRecord activity1 = taskFragment.getChildAt(1).asActivityRecord(); - // Also create a non-embedded activity in the Task. - final ActivityRecord activity2 = new ActivityBuilder(mAtm).build(); - task.addChild(activity2, POSITION_BOTTOM); - prepareActivityForAppTransition(activity0); - prepareActivityForAppTransition(activity1); - prepareActivityForAppTransition(activity2); - doReturn(false).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity0); - doReturn(true).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity1); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(activity1, null /* closingActivity */, taskFragment); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // The animation will be animated remotely by client and all activities are input disabled - // for untrusted animation. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - verify(activity0).setDropInputForAnimation(true); - verify(activity1).setDropInputForAnimation(true); - verify(activity2).setDropInputForAnimation(true); - verify(activity0).setDropInputMode(DropInputMode.ALL); - verify(activity1).setDropInputMode(DropInputMode.ALL); - verify(activity2).setDropInputMode(DropInputMode.ALL); - - // Reset input after animation is finished. - clearInvocations(activity0); - clearInvocations(activity1); - clearInvocations(activity2); - remoteAnimationRunner.finishAnimation(); - - verify(activity0).setDropInputForAnimation(false); - verify(activity1).setDropInputForAnimation(false); - verify(activity2).setDropInputForAnimation(false); - verify(activity0).setDropInputMode(DropInputMode.OBSCURED); - verify(activity1).setDropInputMode(DropInputMode.NONE); - verify(activity2).setDropInputMode(DropInputMode.NONE); - } - - /** - * Since we don't have any use case to rely on handling input during animation, disable it even - * if it is trusted embedding so that it could cover some edge-cases when a previously trusted - * host starts doing something bad. - */ - @Test - public void testOverrideTaskFragmentAdapter_inputProtectedForTrustedAnimation() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with only trusted embedded activity - final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .createActivityCount(1) - .setOrganizer(organizer) - .build(); - final ActivityRecord activity = taskFragment.getChildAt(0).asActivityRecord(); - prepareActivityForAppTransition(activity); - doReturn(true).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // The animation will be animated remotely by client and all activities are input disabled - // for untrusted animation. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - verify(activity).setDropInputForAnimation(true); - verify(activity).setDropInputMode(DropInputMode.ALL); - - // Reset input after animation is finished. - clearInvocations(activity); - remoteAnimationRunner.finishAnimation(); - - verify(activity).setDropInputForAnimation(false); - verify(activity).setDropInputMode(DropInputMode.NONE); - } - - /** - * We don't need to drop input for fully trusted embedding (system app, and embedding in the - * same app). This will allow users to do fast tapping. - */ - @Test - public void testOverrideTaskFragmentAdapter_noInputProtectedForFullyTrustedAnimation() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with only trusted embedded activity - final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .createActivityCount(1) - .setOrganizer(organizer) - .build(); - final ActivityRecord activity = taskFragment.getChildAt(0).asActivityRecord(); - prepareActivityForAppTransition(activity); - final int uid = mAtm.mTaskFragmentOrganizerController.getTaskFragmentOrganizerUid( - getITaskFragmentOrganizer(organizer)); - doReturn(true).when(task).isFullyTrustedEmbedding(uid); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - // The animation will be animated remotely by client, but input should not be dropped for - // fully trusted. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - verify(activity, never()).setDropInputForAnimation(true); - verify(activity, never()).setDropInputMode(DropInputMode.ALL); - } - - @Test public void testTransitionGoodToGoForTaskFragments() { final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); final Task task = createTask(mDisplayContent); @@ -1253,22 +898,6 @@ public class AppTransitionControllerTest extends WindowTestsBase { verify(mDisplayContent.mAppTransition).goodToGo(anyInt(), any()); } - /** Registers remote animation for the organizer. */ - private void setupTaskFragmentRemoteAnimation(TaskFragmentOrganizer organizer, - TestRemoteAnimationRunner remoteAnimationRunner) { - final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( - remoteAnimationRunner, 10, 1); - final ITaskFragmentOrganizer iOrganizer = getITaskFragmentOrganizer(organizer); - final RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); - definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, adapter); - definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, adapter); - definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, adapter); - definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter); - definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_CLOSE, adapter); - registerTaskFragmentOrganizer(iOrganizer); - mAtm.mTaskFragmentOrganizerController.registerRemoteAnimations(iOrganizer, definition); - } - private static ITaskFragmentOrganizer getITaskFragmentOrganizer( TaskFragmentOrganizer organizer) { return ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java index a4df03447754..c9c7e92d71cd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java @@ -18,9 +18,11 @@ package com.android.server.wm; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_DISALLOW; +import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_BOUND_BY_FOREGROUND; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_FOREGROUND; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_GRACE_PERIOD; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION; +import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_TOKEN; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW; import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK; @@ -30,12 +32,18 @@ import android.app.BackgroundStartPrivileges; import android.content.Context; import android.os.Binder; import android.os.IBinder; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.filters.SmallTest; import com.android.server.wm.BackgroundActivityStartController.BalVerdict; +import com.android.window.flags.Flags; +import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -44,6 +52,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.Set; + /** * Tests for the {@link BackgroundLaunchProcessController} class. * @@ -55,6 +64,10 @@ import java.util.Set; @RunWith(JUnit4.class) public class BackgroundLaunchProcessControllerTests { + + @ClassRule public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + @Rule public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + Set<IBinder> mActivityStartAllowed = new HashSet<>(); Set<Integer> mHasActiveVisibleWindow = new HashSet<>(); @@ -113,6 +126,25 @@ public class BackgroundLaunchProcessControllerTests { } @Test + @DisableFlags(Flags.FLAG_BAL_IMPROVED_METRICS) + public void testAllowedByTokenNoCallbackOld() { + mController = new BackgroundLaunchProcessController(mHasActiveVisibleWindow::contains, + null); + Binder token = new Binder(); + mActivityStartAllowed.add(token); + mController.addOrUpdateAllowBackgroundStartPrivileges(token, + BackgroundStartPrivileges.ALLOW_BAL); + BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( + mPid, mUid, mPackageName, + mAppSwitchState, mIsCheckingForFgsStart, + mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, + mLastStopAppSwitchesTime, mLastActivityLaunchTime, + mLastActivityFinishTime); + assertThat(balVerdict.getCode()).isEqualTo(BAL_ALLOW_PERMISSION); + } + + @Test + @EnableFlags(Flags.FLAG_BAL_IMPROVED_METRICS) public void testAllowedByTokenNoCallback() { mController = new BackgroundLaunchProcessController(mHasActiveVisibleWindow::contains, null); @@ -126,10 +158,27 @@ public class BackgroundLaunchProcessControllerTests { mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); + assertThat(balVerdict.getCode()).isEqualTo(BAL_ALLOW_TOKEN); + } + + @Test + @DisableFlags(Flags.FLAG_BAL_IMPROVED_METRICS) + public void testAllowedByTokenOld() { + Binder token = new Binder(); + mActivityStartAllowed.add(token); + mController.addOrUpdateAllowBackgroundStartPrivileges(token, + BackgroundStartPrivileges.ALLOW_BAL); + BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( + mPid, mUid, mPackageName, + mAppSwitchState, mIsCheckingForFgsStart, + mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, + mLastStopAppSwitchesTime, mLastActivityLaunchTime, + mLastActivityFinishTime); assertThat(balVerdict.getCode()).isEqualTo(BAL_ALLOW_PERMISSION); } @Test + @EnableFlags(Flags.FLAG_BAL_IMPROVED_METRICS) public void testAllowedByToken() { Binder token = new Binder(); mActivityStartAllowed.add(token); @@ -141,11 +190,12 @@ public class BackgroundLaunchProcessControllerTests { mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); - assertThat(balVerdict.getCode()).isEqualTo(BAL_ALLOW_PERMISSION); + assertThat(balVerdict.getCode()).isEqualTo(BAL_ALLOW_TOKEN); } @Test - public void testBoundByForeground() { + @DisableFlags(Flags.FLAG_BAL_IMPROVED_METRICS) + public void testBoundByForegroundOld() { mAppSwitchState = APP_SWITCH_ALLOW; mController.addBoundClientUid(999, "visible.package", Context.BIND_ALLOW_ACTIVITY_STARTS); mHasActiveVisibleWindow.add(999); @@ -159,6 +209,21 @@ public class BackgroundLaunchProcessControllerTests { } @Test + @EnableFlags(Flags.FLAG_BAL_IMPROVED_METRICS) + public void testBoundByForeground() { + mAppSwitchState = APP_SWITCH_ALLOW; + mController.addBoundClientUid(999, "visible.package", Context.BIND_ALLOW_ACTIVITY_STARTS); + mHasActiveVisibleWindow.add(999); + BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( + mPid, mUid, mPackageName, + mAppSwitchState, mIsCheckingForFgsStart, + mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, + mLastStopAppSwitchesTime, mLastActivityLaunchTime, + mLastActivityFinishTime); + assertThat(balVerdict.getCode()).isEqualTo(BAL_ALLOW_BOUND_BY_FOREGROUND); + } + + @Test public void testForegroundTask() { mAppSwitchState = APP_SWITCH_ALLOW; mHasActivityInVisibleTask = true; diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java index 83ad7b1b5d92..708d6860abc2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java @@ -214,7 +214,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { final Rect activityBounds = new Rect(mFirstActivity.getBounds()); // DAG is portrait (860x1200), so Task and Activity fill DAG. - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); assertThat(taskBounds).isEqualTo(dagBounds); assertThat(activityBounds).isEqualTo(taskBounds); @@ -238,7 +239,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { new Rect(mFirstActivity.getConfiguration().windowConfiguration.getBounds()); // DAG is landscape (1200x860), no fixed orientation letterbox - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isTrue(); assertThat(newDagBounds.width()).isEqualTo(dagBounds.height()); assertThat(newDagBounds.height()).isEqualTo(dagBounds.width()); @@ -262,11 +264,13 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { mDisplay.onLastFocusedTaskDisplayAreaChanged(mFirstTda); prepareUnresizable(mFirstActivity, SCREEN_ORIENTATION_NOSENSOR); - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); rotateDisplay(mDisplay, ROTATION_90); - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); } @@ -283,7 +287,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { // DAG is portrait (860x1200), and activity is letterboxed for fixed orientation // (860x[860x860/1200=616]). Task fills DAG. - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isTrue(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isTrue(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); assertThat(taskBounds).isEqualTo(dagBounds); assertThat(activityBounds.width()).isEqualTo(dagBounds.width()); @@ -300,7 +305,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { mDisplay.onLastFocusedTaskDisplayAreaChanged(mFirstTda); prepareUnresizable(mFirstActivity, SCREEN_ORIENTATION_NOSENSOR); - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); } @Test @@ -310,7 +316,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { mDisplay.onLastFocusedTaskDisplayAreaChanged(mFirstTda); prepareUnresizable(mFirstActivity, SCREEN_ORIENTATION_LOCKED); - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); } @Test @@ -329,7 +336,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { final Rect newActivityBounds = new Rect(mFirstActivity.getBounds()); // DAG is landscape (1200x860), no fixed orientation letterbox - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isTrue(); assertThat(newDagBounds.width()).isEqualTo(dagBounds.height()); assertThat(newDagBounds.height()).isEqualTo(dagBounds.width()); @@ -354,7 +362,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { rotateDisplay(mDisplay, ROTATION_90); - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); } @@ -522,7 +531,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { assertThat(mDisplay.getLastOrientation()).isEqualTo(SCREEN_ORIENTATION_LANDSCAPE); assertThat(mFirstRoot.getConfiguration().orientation).isEqualTo(ORIENTATION_PORTRAIT); assertThat(mFirstActivity.getConfiguration().orientation).isEqualTo(ORIENTATION_PORTRAIT); - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); // Launch portrait on second DAG @@ -534,13 +544,15 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { assertThat(mDisplay.getLastOrientation()).isEqualTo(SCREEN_ORIENTATION_PORTRAIT); assertThat(mSecondRoot.getConfiguration().orientation).isEqualTo(ORIENTATION_LANDSCAPE); assertThat(mSecondActivity.getConfiguration().orientation).isEqualTo(ORIENTATION_LANDSCAPE); - assertThat(mSecondActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); + assertThat(mSecondActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mSecondActivity.inSizeCompatMode()).isFalse(); // First activity is letterboxed in portrait as requested. assertThat(mFirstRoot.getConfiguration().orientation).isEqualTo(ORIENTATION_LANDSCAPE); assertThat(mFirstActivity.getConfiguration().orientation).isEqualTo(ORIENTATION_PORTRAIT); - assertThat(mFirstActivity.isLetterboxedForFixedOrientationAndAspectRatio()).isTrue(); + assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()).isTrue(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 802f8d206e78..c42367e8ae18 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -293,6 +293,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mainWindow.mInvGlobalScale = 1f; spyOn(resources); spyOn(mActivity); + spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()); if (taskbar != null) { taskbar.setVisible(true); @@ -301,7 +302,8 @@ public class LetterboxUiControllerTest extends WindowTestsBase { doReturn(mLetterboxedPortraitTaskBounds).when(mActivity).getBounds(); doReturn(false).when(mActivity).isInLetterboxAnimation(); doReturn(true).when(mActivity).isVisible(); - doReturn(true).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio(); + doReturn(true).when(mActivity.mAppCompatController + .getAppCompatAspectRatioPolicy()).isLetterboxedForFixedOrientationAndAspectRatio(); doReturn(insets).when(mainWindow).getInsetsState(); doReturn(attrs).when(mainWindow).getAttrs(); doReturn(true).when(mainWindow).isDrawn(); @@ -324,11 +326,11 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE, /* value */ true); - mActivity = setUpActivityWithComponent(); - mController = new LetterboxUiController(mWm, mActivity); doReturn(false).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled(); + mActivity = setUpActivityWithComponent(); - assertFalse(mController.shouldApplyUserFullscreenOverride()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldApplyUserFullscreenOverride()); } @Test @@ -339,9 +341,9 @@ public class LetterboxUiControllerTest extends WindowTestsBase { /* value */ false); mActivity = setUpActivityWithComponent(); - mController = new LetterboxUiController(mWm, mActivity); - assertFalse(mController.shouldApplyUserFullscreenOverride()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldApplyUserFullscreenOverride()); } @Test @@ -351,16 +353,17 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ false); mActivity = setUpActivityWithComponent(); - mController = new LetterboxUiController(mWm, mActivity); - assertFalse(mController.shouldApplyUserFullscreenOverride()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldApplyUserFullscreenOverride()); } @Test public void testShouldApplyUserFullscreenOverride_returnsTrue() { prepareActivityThatShouldApplyUserFullscreenOverride(); - assertTrue(mController.shouldApplyUserFullscreenOverride()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldApplyUserFullscreenOverride()); } @Test @@ -421,9 +424,9 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ true); mActivity = setUpActivityWithComponent(); - mController = new LetterboxUiController(mWm, mActivity); - assertFalse(mController.shouldApplyUserMinAspectRatioOverride()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldApplyUserMinAspectRatioOverride()); } @Test @@ -431,21 +434,24 @@ public class LetterboxUiControllerTest extends WindowTestsBase { prepareActivityThatShouldApplyUserMinAspectRatioOverride(); mDisplayContent.setIgnoreOrientationRequest(false); - assertFalse(mController.shouldApplyUserMinAspectRatioOverride()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldApplyUserMinAspectRatioOverride()); } @Test public void testShouldApplyUserMinAspectRatioOverride_returnsTrue() { prepareActivityThatShouldApplyUserMinAspectRatioOverride(); - assertTrue(mController.shouldApplyUserMinAspectRatioOverride()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldApplyUserMinAspectRatioOverride()); } @Test public void testShouldApplyUserMinAspectRatioOverride_noIgnoreOrientation_returnsFalse() { prepareActivityForShouldApplyUserMinAspectRatioOverride(/* orientationRequest */ false); - assertFalse(mController.shouldApplyUserMinAspectRatioOverride()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldApplyUserMinAspectRatioOverride()); } private void prepareActivityForShouldApplyUserMinAspectRatioOverride( @@ -591,9 +597,10 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @Test @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) public void testshouldOverrideMinAspectRatio_overrideEnabled_returnsTrue() { - mController = new LetterboxUiController(mWm, mActivity); + mActivity = setUpActivityWithComponent(); - assertTrue(mController.shouldOverrideMinAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldOverrideMinAspectRatio()); } @Test @@ -601,9 +608,10 @@ public class LetterboxUiControllerTest extends WindowTestsBase { public void testshouldOverrideMinAspectRatio_propertyTrue_overrideEnabled_returnsTrue() throws Exception { mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); - mController = new LetterboxUiController(mWm, mActivity); + mActivity = setUpActivityWithComponent(); - assertTrue(mController.shouldOverrideMinAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldOverrideMinAspectRatio()); } @Test @@ -611,17 +619,19 @@ public class LetterboxUiControllerTest extends WindowTestsBase { public void testshouldOverrideMinAspectRatio_propertyTrue_overrideDisabled_returnsFalse() throws Exception { mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); - mController = new LetterboxUiController(mWm, mActivity); + mActivity = setUpActivityWithComponent(); - assertFalse(mController.shouldOverrideMinAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldOverrideMinAspectRatio()); } @Test @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) public void testshouldOverrideMinAspectRatio_overrideDisabled_returnsFalse() { - mController = new LetterboxUiController(mWm, mActivity); + mActivity = setUpActivityWithComponent(); - assertFalse(mController.shouldOverrideMinAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldOverrideMinAspectRatio()); } @Test @@ -631,9 +641,9 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); mActivity = setUpActivityWithComponent(); - mController = new LetterboxUiController(mWm, mActivity); - assertFalse(mController.shouldOverrideMinAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldOverrideMinAspectRatio()); } @Test @@ -641,9 +651,10 @@ public class LetterboxUiControllerTest extends WindowTestsBase { public void testshouldOverrideMinAspectRatio_propertyFalse_noOverride_returnsFalse() throws Exception { mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); - mController = new LetterboxUiController(mWm, mActivity); + mActivity = setUpActivityWithComponent(); - assertFalse(mController.shouldOverrideMinAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() + .shouldOverrideMinAspectRatio()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java index 3078df026d8a..d29505f02fe8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java @@ -274,17 +274,28 @@ public class RootWindowContainerTests extends WindowTestsBase { @Test public void testAttachApplication() { - final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final ActivityRecord activity = new ActivityBuilder(mAtm).setProcessName("testAttach") + .setCreateTask(true).build(); + final ActivityRecord topActivity = new ActivityBuilder(mAtm).setProcessName("testAttach") + .setUseProcess(activity.app).setTask(activity.getTask()).build(); activity.detachFromProcess(); - mAtm.startProcessAsync(activity, false /* knownToBeDead */, + topActivity.detachFromProcess(); + mAtm.startProcessAsync(topActivity, false /* knownToBeDead */, true /* isTop */, "test" /* hostingType */); + // Even if the activity is added after topActivity, the start order should still follow + // z-order, i.e. the topActivity will be started first. + mAtm.startProcessAsync(activity, false /* knownToBeDead */, + false /* isTop */, "test" /* hostingType */); + assertEquals(2, mAtm.mStartingProcessActivities.size()); + assertEquals("Top record must be at the tail to start first", + topActivity, mAtm.mStartingProcessActivities.get(1)); final WindowProcessController proc = mSystemServicesTestRule.addProcess( activity.packageName, activity.processName, 6789 /* pid */, activity.info.applicationInfo.uid); try { mRootWindowContainer.attachApplication(proc); - verify(mSupervisor).realStartActivityLocked(eq(activity), eq(proc), anyBoolean(), - anyBoolean()); + verify(mSupervisor).realStartActivityLocked(eq(topActivity), eq(proc), + anyBoolean(), anyBoolean()); } catch (RemoteException e) { e.rethrowAsRuntimeException(); } @@ -428,7 +439,8 @@ public class RootWindowContainerTests extends WindowTestsBase { final Rect bounds = new Rect(task.getBounds()); bounds.scale(0.5f); task.setBounds(bounds); - assertFalse(activity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(task.autoRemoveRecents).isFalse(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 0eb3edbad73c..ae88b1baa5b8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -64,6 +64,7 @@ import static com.android.server.wm.ActivityRecord.State.PAUSED; import static com.android.server.wm.ActivityRecord.State.RESTARTING_PROCESS; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityRecord.State.STOPPED; +import static com.android.server.wm.AppCompatUtils.computeAspectRatio; import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER; import static com.android.server.wm.WindowContainer.POSITION_TOP; @@ -103,6 +104,7 @@ import android.graphics.Rect; import android.os.Binder; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; @@ -494,7 +496,7 @@ public class SizeCompatTests extends WindowTestsBase { // Activity is sandboxed; it is in size compat mode since it is not resizable and has a // max aspect ratio. assertActivityMaxBoundsSandboxed(); - assertScaled(); + assertDownScaled(); } @Test @@ -514,7 +516,7 @@ public class SizeCompatTests extends WindowTestsBase { // The bounds should be [100, 0 - 1100, 2500]. assertEquals(origBounds.width(), currentBounds.width()); assertEquals(origBounds.height(), currentBounds.height()); - assertScaled(); + assertDownScaled(); // The scale is 2000/2500=0.8. The horizontal centered offset is (1000-(1000*0.8))/2=100. final float scale = (float) display.mBaseDisplayHeight / currentBounds.height(); @@ -554,7 +556,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(origBounds.width(), currentBounds.width()); assertEquals(origBounds.height(), currentBounds.height()); assertEquals(offsetX, mActivity.getBounds().left); - assertScaled(); + assertDownScaled(); // Activity is sandboxed due to size compat mode. assertActivityMaxBoundsSandboxed(); @@ -692,7 +694,7 @@ public class SizeCompatTests extends WindowTestsBase { // The configuration bounds [820, 0 - 1820, 2500] should keep the same. assertEquals(originalBounds.width(), currentBounds.width()); assertEquals(originalBounds.height(), currentBounds.height()); - assertScaled(); + assertDownScaled(); // Activity max bounds are sandboxed due to size compat mode on the new display. assertActivityMaxBoundsSandboxed(); @@ -751,7 +753,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(origAppBounds.width(), appBounds.width()); assertEquals(origAppBounds.height(), appBounds.height()); // The activity is 1000x1400 and the display is 2500x1000. - assertScaled(); + assertDownScaled(); final float scale = mActivity.getCompatScale(); // The position in configuration should be in app coordinates. final Rect screenBounds = mActivity.getBounds(); @@ -848,7 +850,7 @@ public class SizeCompatTests extends WindowTestsBase { // Size compatibility mode is able to handle orientation change so the process shouldn't be // restarted and the override configuration won't be cleared. verify(mActivity, never()).restartProcessIfVisible(); - assertScaled(); + assertDownScaled(); // Activity max bounds are sandboxed due to size compat mode, even if is not visible. assertActivityMaxBoundsSandboxed(); @@ -1103,9 +1105,10 @@ public class SizeCompatTests extends WindowTestsBase { RESIZE_MODE_UNRESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); // Simulate the user selecting the fullscreen user aspect ratio override - spyOn(activity.mLetterboxUiController); - doReturn(true).when(activity.mLetterboxUiController) - .isSystemOverrideToFullscreenEnabled(); + spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides()); + doReturn(true).when( + activity.mAppCompatController.getAppCompatAspectRatioOverrides()) + .isSystemOverrideToFullscreenEnabled(); assertFalse(activity.shouldCreateCompatDisplayInsets()); } @@ -1124,7 +1127,8 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(activity, /* maxAspect=*/ 1.5f, SCREEN_ORIENTATION_LANDSCAPE); // Activity max bounds should not be sandboxed, even though it is letterboxed. - assertTrue(activity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(activity.getConfiguration().windowConfiguration.getMaxBounds()) .isEqualTo(activity.getDisplayArea().getBounds()); } @@ -1165,7 +1169,8 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(activity, /* maxAspect=*/ 1.5f, SCREEN_ORIENTATION_LANDSCAPE); // Activity max bounds should not be sandboxed, even though it is letterboxed. - assertTrue(activity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(activity.getConfiguration().windowConfiguration.getMaxBounds()) .isEqualTo(activity.getDisplayArea().getBounds()); } @@ -1188,7 +1193,8 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(activity, /* maxAspect=*/ 1.5f, SCREEN_ORIENTATION_LANDSCAPE); // Activity max bounds should not be sandboxed, even though it is letterboxed. - assertTrue(activity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(activity.getConfiguration().windowConfiguration.getMaxBounds()) .isEqualTo(activity.getDisplayArea().getBounds()); } @@ -1619,6 +1625,85 @@ public class SizeCompatTests extends WindowTestsBase { activity.getBounds().width(), 0.5); } + + /** + * Test that a freeform unresizeable activity can be down-scaled to fill its smaller parent + * bounds. + */ + @Test + public void testCompatScaling_freeformUnresizeableApp_largerThanParent_downScaled() { + final int dw = 600; + final int dh = 800; + final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh) + .setWindowingMode(WINDOWING_MODE_FREEFORM) + .build(); + setUpApp(display); + prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT); + mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM); + assertFalse(mActivity.inSizeCompatMode()); + + // Resize app to make original app bounds larger than parent bounds. + mTask.getWindowConfiguration().setAppBounds( + new Rect(0, 0, dw - 300, dh - 400)); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + // App should enter size compat mode and be down-scaled to fill new parent bounds. + assertDownScaled(); + } + + /** + * Test that when desktop mode is enabled, a freeform unresizeable activity can be up-scaled to + * fill its larger parent bounds. + */ + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void testCompatScaling_freeformUnresizeableApp_smallerThanParent_upScaled() { + doReturn(true).when(() -> + DesktopModeLaunchParamsModifier.canEnterDesktopMode(any())); + final int dw = 600; + final int dh = 800; + final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh) + .setWindowingMode(WINDOWING_MODE_FREEFORM) + .build(); + setUpApp(display); + prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT); + mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM); + assertFalse(mActivity.inSizeCompatMode()); + + // Resize app to make original app bounds smaller than parent bounds. + mTask.getWindowConfiguration().setAppBounds( + new Rect(0, 0, dw + 300, dh + 400)); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + // App should enter size compat mode and be up-scaled to fill parent bounds. + assertUpScaled(); + } + + /** + * Test that when desktop mode is disabled, a freeform unresizeable activity cannot be up-scaled + * despite its larger parent bounds. + */ + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void testSizeCompatScaling_freeformUnresizeableApp_smallerThanParent_notScaled() { + final int dw = 600; + final int dh = 800; + final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh) + .setWindowingMode(WINDOWING_MODE_FREEFORM) + .build(); + setUpApp(display); + prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT); + mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM); + assertFalse(mActivity.inSizeCompatMode()); + final Rect originalAppBounds = mActivity.getBounds(); + + // Resize app to make original app bounds smaller than parent bounds. + mTask.getWindowConfiguration().setAppBounds( + new Rect(0, 0, dw + 300, dh + 400)); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + // App should enter size compat mode but remain its original size. + assertTrue(mActivity.inSizeCompatMode()); + assertEquals(originalAppBounds, mActivity.getBounds()); + } + @Test public void testGetLetterboxInnerBounds_noScalingApplied() { // Set up a display in portrait and ignoring orientation request. @@ -1637,7 +1722,8 @@ public class SizeCompatTests extends WindowTestsBase { addWindowToActivity(mActivity); // App should launch in fullscreen. - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); // Activity inherits max bounds from TaskDisplayArea. @@ -1651,8 +1737,9 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(rotatedDisplayBounds.width() < rotatedDisplayBounds.height()); // App should be in size compat. - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); - assertScaled(); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); + assertDownScaled(); assertThat(mActivity.inSizeCompatMode()).isTrue(); assertActivityMaxBoundsSandboxed(); @@ -1763,7 +1850,8 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() > displayBounds.height()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertActivityMaxBoundsSandboxed(); @@ -1794,7 +1882,8 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() > displayBounds.height()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); // Activity bounds should respect minimum aspect ratio for activity. @@ -1824,7 +1913,8 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() > displayBounds.height()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); // Activity bounds should respect maximum aspect ratio for activity. @@ -1854,7 +1944,8 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() > displayBounds.height()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); // Activity bounds should respect aspect ratio override for fixed orientation letterbox. @@ -1951,7 +2042,8 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() > displayBounds.height()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); // Letterbox logic should use config_letterboxDefaultMinAspectRatioForUnresizableApps over @@ -1977,7 +2069,8 @@ public class SizeCompatTests extends WindowTestsBase { // App should launch in fixed orientation letterbox. // Activity bounds should be 700x1400 with the ratio as the display. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFitted(); assertEquals(originalScreenWidthDp, mActivity.getConfiguration().smallestScreenWidthDp); assertTrue(originalScreenWidthDp < originalScreenHeighthDp); @@ -1987,7 +2080,7 @@ public class SizeCompatTests extends WindowTestsBase { // After we rotate, the activity should go in the size-compat mode and report the same // configuration values. - assertScaled(); + assertDownScaled(); assertEquals(originalScreenWidthDp, mActivity.getConfiguration().smallestScreenWidthDp); assertEquals(originalScreenWidthDp, mActivity.getConfiguration().screenWidthDp); assertEquals(originalScreenHeighthDp, mActivity.getConfiguration().screenHeightDp); @@ -2019,7 +2112,8 @@ public class SizeCompatTests extends WindowTestsBase { // App should launch in fixed orientation letterbox. // Activity bounds should be 700x1400 with the ratio as the display. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFitted(); assertEquals(originalScreenWidthDp, mActivity.getConfiguration().smallestScreenWidthDp); assertTrue(originalScreenWidthDp < originalScreenHeighthDp); @@ -2053,7 +2147,8 @@ public class SizeCompatTests extends WindowTestsBase { final Rect activityBounds = new Rect(mActivity.getBounds()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); @@ -2593,7 +2688,8 @@ public class SizeCompatTests extends WindowTestsBase { final Rect activityBounds = new Rect(mActivity.getBounds()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); @@ -2637,13 +2733,14 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); // Check that the display aspect ratio is used by the app. final float targetMinAspectRatio = 1f * displayHeight / displayWidth; - assertEquals(targetMinAspectRatio, ActivityRecord - .computeAspectRatio(mActivity.getBounds()), DELTA_ASPECT_RATIO_TOLERANCE); + assertEquals(targetMinAspectRatio, computeAspectRatio(mActivity.getBounds()), + DELTA_ASPECT_RATIO_TOLERANCE); } @Test @@ -2672,13 +2769,14 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); // Check that the display aspect ratio is used by the app. final float targetMinAspectRatio = 1f * displayWidth / displayHeight; - assertEquals(targetMinAspectRatio, ActivityRecord - .computeAspectRatio(mActivity.getBounds()), DELTA_ASPECT_RATIO_TOLERANCE); + assertEquals(targetMinAspectRatio, computeAspectRatio(mActivity.getBounds()), + DELTA_ASPECT_RATIO_TOLERANCE); } @Test @@ -2698,13 +2796,14 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); // Check that the display aspect ratio is used by the app. final float targetMinAspectRatio = 1f * displayHeight / displayWidth; - assertEquals(targetMinAspectRatio, ActivityRecord - .computeAspectRatio(mActivity.getBounds()), DELTA_ASPECT_RATIO_TOLERANCE); + assertEquals(targetMinAspectRatio, computeAspectRatio(mActivity.getBounds()), + DELTA_ASPECT_RATIO_TOLERANCE); } @Test @@ -2724,13 +2823,14 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); // Check that the display aspect ratio is used by the app. final float targetMinAspectRatio = 1f * displayWidth / displayHeight; - assertEquals(targetMinAspectRatio, ActivityRecord - .computeAspectRatio(mActivity.getBounds()), DELTA_ASPECT_RATIO_TOLERANCE); + assertEquals(targetMinAspectRatio, computeAspectRatio(mActivity.getBounds()), + DELTA_ASPECT_RATIO_TOLERANCE); } @Test @@ -2753,8 +2853,9 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() < displayBounds.height()); // App should be in size compat. - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); - assertScaled(); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); + assertDownScaled(); assertEquals(activityBounds.width(), newActivityBounds.width()); assertEquals(activityBounds.height(), newActivityBounds.height()); assertActivityMaxBoundsSandboxed(); @@ -2770,7 +2871,8 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); // App should launch in fullscreen. - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); // Activity inherits max bounds from TaskDisplayArea. assertMaxBoundsInheritDisplayAreaBounds(); @@ -2783,8 +2885,9 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(rotatedDisplayBounds.width() > rotatedDisplayBounds.height()); // App should be in size compat. - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); - assertScaled(); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); + assertDownScaled(); assertThat(mActivity.inSizeCompatMode()).isTrue(); assertActivityMaxBoundsSandboxed(); @@ -2804,7 +2907,8 @@ public class SizeCompatTests extends WindowTestsBase { // Portrait fixed app without max aspect. prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); // Launch another portrait fixed app. @@ -2826,7 +2930,8 @@ public class SizeCompatTests extends WindowTestsBase { // Task and display bounds should be equal while activity should be letterboxed and // has 700x1400 bounds with the ratio as the display. - assertTrue(newActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(newActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(newActivity.inSizeCompatMode()); // Activity max bounds are sandboxed due to size compat mode. assertThat(newActivity.getConfiguration().windowConfiguration.getMaxBounds()) @@ -2847,7 +2952,8 @@ public class SizeCompatTests extends WindowTestsBase { // Portrait fixed app without max aspect. prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); mActivity.setRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED); @@ -2868,7 +2974,8 @@ public class SizeCompatTests extends WindowTestsBase { // Portrait fixed app without max aspect. prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); // Launch another portrait fixed app with max aspect ratio as 1.3. @@ -2898,7 +3005,8 @@ public class SizeCompatTests extends WindowTestsBase { assertActivityMaxBoundsSandboxed(); // Activity bounds should be (1400 / 1.3 = 1076)x1400 with the app requested ratio. - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(newActivity.inSizeCompatMode()); assertEquals(displayBounds.height(), newActivityBounds.height()); assertEquals((long) Math.rint(newActivityBounds.height() @@ -2917,15 +3025,17 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); clearInvocations(mActivity); - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); // Rotate display to portrait. rotateDisplay(mActivity.mDisplayContent, ROTATION_90); // App should be in size compat. - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); - assertScaled(); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); + assertDownScaled(); assertThat(mActivity.inSizeCompatMode()).isTrue(); // Activity max bounds are sandboxed due to size compat mode. assertActivityMaxBoundsSandboxed(); @@ -2935,8 +3045,9 @@ public class SizeCompatTests extends WindowTestsBase { // App still in size compat, and the bounds don't change. verify(mActivity, never()).clearSizeCompatMode(); - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); - assertScaled(); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); + assertDownScaled(); assertEquals(activityBounds, mActivity.getBounds()); // Activity max bounds are sandboxed due to size compat. assertActivityMaxBoundsSandboxed(); @@ -2953,7 +3064,8 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); // In fixed orientation letterbox - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertActivityMaxBoundsSandboxed(); @@ -2961,15 +3073,17 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(display, ROTATION_90); // App should be in size compat. - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); - assertScaled(); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); + assertDownScaled(); assertActivityMaxBoundsSandboxed(); // Rotate display to landscape. rotateDisplay(display, ROTATION_180); // In activity letterbox - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertActivityMaxBoundsSandboxed(); } @@ -2987,7 +3101,8 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_LANDSCAPE); // In fixed orientation letterbox - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertActivityMaxBoundsSandboxed(); @@ -2995,15 +3110,17 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(display, ROTATION_90); // App should be in size compat. - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); - assertScaled(); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); + assertDownScaled(); assertActivityMaxBoundsSandboxed(); // Rotate display to portrait. rotateDisplay(display, ROTATION_180); // In fixed orientation letterbox - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertActivityMaxBoundsSandboxed(); } @@ -3187,7 +3304,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // Non-resizable activity in size compat mode - assertScaled(); + assertDownScaled(); final Rect newBounds = new Rect(mActivity.getWindowConfiguration().getBounds()); assertEquals(originalBounds.width(), newBounds.width()); assertEquals(originalBounds.height(), newBounds.height()); @@ -3200,7 +3317,8 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(ORIENTATION_PORTRAIT, mTask.getConfiguration().orientation); assertEquals(ORIENTATION_LANDSCAPE, mActivity.getConfiguration().orientation); assertFitted(); - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertActivityMaxBoundsSandboxed(); // Letterbox should fill the gap between the split screen and the letterboxed activity. @@ -3226,7 +3344,8 @@ public class SizeCompatTests extends WindowTestsBase { // Resizable activity is not in size compat mode but in the letterbox for fixed orientation. assertFitted(); - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); } @Test @@ -3249,7 +3368,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // Non-resizable activity in size compat mode - assertScaled(); + assertDownScaled(); final Rect newBounds = new Rect(mActivity.getWindowConfiguration().getBounds()); assertEquals(originalBounds.width(), newBounds.width()); assertEquals(originalBounds.height(), newBounds.height()); @@ -3262,7 +3381,8 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(ORIENTATION_PORTRAIT, mTask.getConfiguration().orientation); assertEquals(ORIENTATION_PORTRAIT, mActivity.getConfiguration().orientation); assertFitted(); - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertActivityMaxBoundsSandboxed(); // Activity bounds fill split screen. @@ -3289,7 +3409,7 @@ public class SizeCompatTests extends WindowTestsBase { organizer.mPrimary.setBounds(0, 0, 1000, 800); // Non-resizable activity should be in size compat mode. - assertScaled(); + assertDownScaled(); assertEquals(mActivity.getBounds(), new Rect(60, 0, 940, 800)); recomputeNaturalConfigurationOfUnresizableActivity(); @@ -3866,7 +3986,7 @@ public class SizeCompatTests extends WindowTestsBase { // Force activity to scaled down for size compat mode. resizeDisplay(mTask.mDisplayContent, 700, 1400); assertTrue(mActivity.inSizeCompatMode()); - assertScaled(); + assertDownScaled(); assertEquals(sizeCompatScaled, mActivity.getBounds()); } @@ -3897,7 +4017,8 @@ public class SizeCompatTests extends WindowTestsBase { // orientation is not respected with insets as insets have been decoupled. final Rect appBounds = activity.getWindowConfiguration().getAppBounds(); final Rect displayBounds = display.getBounds(); - assertFalse(activity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertNotNull(appBounds); assertEquals(displayBounds.width(), appBounds.width()); assertEquals(displayBounds.height(), appBounds.height()); @@ -3928,7 +4049,8 @@ public class SizeCompatTests extends WindowTestsBase { final Rect bounds = activity.getBounds(); // Activity should be letterboxed and should have portrait app bounds - assertTrue(activity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertTrue(bounds.height() > bounds.width()); } @@ -3962,7 +4084,8 @@ public class SizeCompatTests extends WindowTestsBase { assertNotNull(activity.getCompatDisplayInsets()); // Activity is not letterboxed for fixed orientation because orientation is respected // with insets, and should not be in size compat mode - assertFalse(activity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(activity.inSizeCompatMode()); } @@ -4363,7 +4486,7 @@ public class SizeCompatTests extends WindowTestsBase { resizeDisplay(mTask.mDisplayContent, 1400, 700); assertTrue(mActivity.inSizeCompatMode()); - assertScaled(); + assertDownScaled(); assertEquals(sizeCompatScaled, mActivity.getBounds()); } @@ -4386,7 +4509,8 @@ public class SizeCompatTests extends WindowTestsBase { verifyLogAppCompatState(mActivity, APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED); prepareUnresizable(mActivity, /* maxAspect= */ 2, SCREEN_ORIENTATION_PORTRAIT); - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertTrue(mActivity.areBoundsLetterboxed()); @@ -4402,7 +4526,8 @@ public class SizeCompatTests extends WindowTestsBase { // ActivityRecord#resolveSizeCompatModeConfiguration because mCompatDisplayInsets aren't // null but activity doesn't enter size compat mode. Checking that areBoundsLetterboxed() // still returns true because of the aspect ratio restrictions. - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertTrue(mActivity.areBoundsLetterboxed()); verifyLogAppCompatState(mActivity, @@ -4429,7 +4554,8 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertTrue(mActivity.areBoundsLetterboxed()); verifyLogAppCompatState(mActivity, @@ -4447,7 +4573,8 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(mActivity.mDisplayContent, ROTATION_90); - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertTrue(mActivity.inSizeCompatMode()); assertTrue(mActivity.areBoundsLetterboxed()); verifyLogAppCompatState(mActivity, @@ -4508,7 +4635,8 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); assertFalse(mActivity.isEligibleForLetterboxEducation()); - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); } @Test @@ -4566,7 +4694,8 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); assertTrue(mActivity.isEligibleForLetterboxEducation()); - assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); } @Test @@ -4580,7 +4709,8 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(mActivity.mDisplayContent, ROTATION_90); assertTrue(mActivity.isEligibleForLetterboxEducation()); - assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertTrue(mActivity.inSizeCompatMode()); } @@ -4622,7 +4752,7 @@ public class SizeCompatTests extends WindowTestsBase { // Target min aspect ratio must be larger than parent aspect ratio to be applied. final float targetMinAspectRatio = 3.0f; - // Create fixed portait activity with min aspect ratio greater than parent aspect ratio. + // Create fixed portrait activity with min aspect ratio greater than parent aspect ratio. final ActivityRecord fixedOrientationActivity = new ActivityBuilder(mAtm) .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .setMinAspectRatio(targetMinAspectRatio).build(); @@ -4636,7 +4766,7 @@ public class SizeCompatTests extends WindowTestsBase { final Rect minAspectRatioAppBounds = new Rect(minAspectRatioActivity.getConfiguration() .windowConfiguration.getAppBounds()); - // Create unresizeable fixed portait activity with min aspect ratio greater than parent + // Create unresizeable fixed portrait activity with min aspect ratio greater than parent // aspect ratio. final ActivityRecord sizeCompatActivity = new ActivityBuilder(mAtm) .setTask(task).setResizeMode(RESIZE_MODE_UNRESIZEABLE) @@ -4648,12 +4778,12 @@ public class SizeCompatTests extends WindowTestsBase { .windowConfiguration.getAppBounds()); // Check that aspect ratio of app bounds is equal to the min aspect ratio. - assertEquals(targetMinAspectRatio, ActivityRecord - .computeAspectRatio(fixedOrientationAppBounds), DELTA_ASPECT_RATIO_TOLERANCE); - assertEquals(targetMinAspectRatio, ActivityRecord - .computeAspectRatio(minAspectRatioAppBounds), DELTA_ASPECT_RATIO_TOLERANCE); - assertEquals(targetMinAspectRatio, ActivityRecord - .computeAspectRatio(sizeCompatAppBounds), DELTA_ASPECT_RATIO_TOLERANCE); + assertEquals(targetMinAspectRatio, + computeAspectRatio(fixedOrientationAppBounds), DELTA_ASPECT_RATIO_TOLERANCE); + assertEquals(targetMinAspectRatio, + computeAspectRatio(minAspectRatioAppBounds), DELTA_ASPECT_RATIO_TOLERANCE); + assertEquals(targetMinAspectRatio, + computeAspectRatio(sizeCompatAppBounds), DELTA_ASPECT_RATIO_TOLERANCE); } @Test @@ -4669,7 +4799,7 @@ public class SizeCompatTests extends WindowTestsBase { // Activity should enter size compat with old density after display density change. display.setForcedDensity(newDensity, UserHandle.USER_CURRENT); - assertScaled(); + assertDownScaled(); assertEquals(origDensity, mActivity.getConfiguration().densityDpi); // Activity should exit size compat with new density. @@ -4908,14 +5038,25 @@ public class SizeCompatTests extends WindowTestsBase { } } - private void assertScaled() { - assertScaled(mActivity); + private void assertUpScaled() { + assertScaled(mActivity, /* upScalingExpected */ true); + } + + private void assertDownScaled() { + assertScaled(mActivity, /* upScalingExpected */ false); } - /** Asserts that the size of activity is larger than its parent so it is scaling. */ - private void assertScaled(ActivityRecord activity) { + /** + * Asserts that the size of an activity differs from its parent and so it is scaling (either up + * or down). + */ + private void assertScaled(ActivityRecord activity, boolean upScalingExpected) { assertTrue(activity.inSizeCompatMode()); - assertNotEquals(1f, activity.getCompatScale(), 0.0001f /* delta */); + if (upScalingExpected) { + assertTrue(activity.getCompatScale() > 1f); + } else { + assertTrue(activity.getCompatScale() < 1f); + } } private void assertFitted() { diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index a71b81e025d2..d013053a063d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -90,7 +90,6 @@ import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; -import android.view.RemoteAnimationDefinition; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.ITaskFragmentOrganizer; @@ -140,7 +139,6 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { private IBinder mFragmentToken; private WindowContainerTransaction mTransaction; private WindowContainerToken mFragmentWindowToken; - private RemoteAnimationDefinition mDefinition; private IBinder mErrorToken; private Rect mTaskFragBounds; @@ -169,7 +167,6 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { mTransaction = new WindowContainerTransaction(); mTransaction.setTaskFragmentOrganizer(mIOrganizer); mFragmentWindowToken = mTaskFragment.mRemoteToken.toWindowContainerToken(); - mDefinition = new RemoteAnimationDefinition(); mErrorToken = new Binder(); final Rect displayBounds = mDisplayContent.getBounds(); mTaskFragBounds = new Rect(displayBounds.left, displayBounds.top, displayBounds.centerX(), @@ -579,17 +576,6 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test - public void testRegisterRemoteAnimations() { - mController.registerRemoteAnimations(mIOrganizer, mDefinition); - - assertEquals(mDefinition, mController.getRemoteAnimationDefinition(mIOrganizer)); - - mController.unregisterRemoteAnimations(mIOrganizer); - - assertNull(mController.getRemoteAnimationDefinition(mIOrganizer)); - } - - @Test public void testApplyTransaction_disallowRemoteTransitionForNonSystemOrganizer() { mTransaction.setRelativeBounds(mFragmentWindowToken, new Rect(0, 0, 100, 100)); mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 35b6b70cb611..47d34a6e5f2b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -759,20 +759,24 @@ public class TaskFragmentTest extends WindowTestsBase { // Assert fixed orientation request is ignored for activity in ActivityEmbedding split. activity0.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); - assertFalse(activity0.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(activity0.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation()); activity1.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT); - assertFalse(activity1.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(activity1.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation()); // Also verify the behavior on device that ignore orientation request. mDisplayContent.setIgnoreOrientationRequest(true); task.onConfigurationChanged(task.getParent().getConfiguration()); - assertFalse(activity0.isLetterboxedForFixedOrientationAndAspectRatio()); - assertFalse(activity1.isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(activity0.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); + assertFalse(activity1.mAppCompatController.getAppCompatAspectRatioPolicy() + .isLetterboxedForFixedOrientationAndAspectRatio()); assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation()); tf0.setResumedActivity(activity0, "test"); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index a94b58690775..401964c2f597 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -22,6 +22,7 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.WindowInsets.Type.systemOverlays; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; @@ -77,6 +78,7 @@ import android.os.Parcel; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; @@ -91,6 +93,8 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; + import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -801,18 +805,11 @@ public class WindowContainerTests extends WindowTestsBase { final TestWindowContainer root2 = builder.setLayer(0).build(); + assertEquals("Roots have the same z-order", 0, root.compareTo(root2)); assertEquals(0, root.compareTo(root)); assertEquals(-1, child1.compareTo(child2)); assertEquals(1, child2.compareTo(child1)); - boolean inTheSameTree = true; - try { - root.compareTo(root2); - } catch (IllegalArgumentException e) { - inTheSameTree = false; - } - assertFalse(inTheSameTree); - assertEquals(-1, child1.compareTo(child11)); assertEquals(1, child21.compareTo(root)); assertEquals(1, child21.compareTo(child12)); @@ -960,6 +957,25 @@ public class WindowContainerTests extends WindowTestsBase { assertTrue(child.handlesOrientationChangeFromDescendant(orientation)); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION) + public void testAddLocalInsets_addsFlagsFromProvider() { + final Task rootTask = createTask(mDisplayContent); + final Task task = createTaskInRootTask(rootTask, 0 /* userId */); + + final Binder owner = new Binder(); + Rect insetsRect = new Rect(0, 200, 1080, 700); + final int flags = FLAG_FORCE_CONSUMING; + final InsetsFrameProvider provider = + new InsetsFrameProvider(owner, 1, WindowInsets.Type.captionBar()) + .setArbitraryRectangle(insetsRect) + .setFlags(flags); + task.addLocalInsetsFrameProvider(provider, owner); + + final int sourceFlags = task.mLocalInsetsSources.get(provider.getId()).getFlags(); + assertEquals(flags, sourceFlags); + } + private static void addLocalInsets(WindowContainer wc) { final Binder owner = new Binder(); Rect genericOverlayInsetsRect1 = new Rect(0, 200, 1080, 700); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index fcf7a3fe79c1..4958b904f326 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -1294,8 +1294,6 @@ public class WindowManagerServiceTests extends WindowTestsBase { @Test public void testRelayout_appWindowSendActivityWindowInfo() { - mSetFlagsRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); - // Skip unnecessary operations of relayout. spyOn(mWm.mWindowPlacerLocked); doNothing().when(mWm.mWindowPlacerLocked).performSurfacePlacement(anyBoolean()); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index d3504cc1bd28..fb81a52bce85 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -30,6 +30,7 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; +import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST; @@ -75,10 +76,12 @@ import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.util.ArrayMap; import android.util.Rational; import android.view.Display; +import android.view.InsetsSource; import android.view.SurfaceControl; import android.view.WindowInsets; import android.window.ITaskFragmentOrganizer; @@ -904,7 +907,8 @@ public class WindowOrganizerTests extends WindowTestsBase { 0 /* index */, WindowInsets.Type.systemOverlays(), new Rect(0, 0, 1080, 200), - null /* boundingRects */); + null /* boundingRects */, + 0 /* flags */); mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); assertThat(navigationBarInsetsReceiverTask.mLocalInsetsSources @@ -930,7 +934,8 @@ public class WindowOrganizerTests extends WindowTestsBase { 0 /* index */, WindowInsets.Type.systemOverlays(), new Rect(0, 0, 1080, 200), - boundingRects); + boundingRects, + 0 /* flags */); mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); assertArrayEquals(boundingRects, navigationBarInsetsReceiverTask.mLocalInsetsSources @@ -938,6 +943,30 @@ public class WindowOrganizerTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION) + public void testAddInsetsSource_withFlags() { + final Task rootTask = createTask(mDisplayContent); + + final Task insetsReceiverTask = createTaskInRootTask(rootTask, 0); + insetsReceiverTask.getConfiguration().windowConfiguration + .setBounds(new Rect(0, 200, 1080, 700)); + + final @InsetsSource.Flags int flags = FLAG_FORCE_CONSUMING; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.addInsetsSource( + insetsReceiverTask.mRemoteToken.toWindowContainerToken(), + new Binder(), + 0 /* index */, + WindowInsets.Type.systemOverlays(), + new Rect(0, 0, 1080, 200), + null /* boundingRects */, + flags); + mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); + + assertEquals(flags, insetsReceiverTask.mLocalInsetsSources.valueAt(0).getFlags()); + } + + @Test public void testRemoveInsetsSource() { final Task rootTask = createTask(mDisplayContent); @@ -952,7 +981,8 @@ public class WindowOrganizerTests extends WindowTestsBase { 0 /* index */, WindowInsets.Type.systemOverlays(), new Rect(0, 0, 1080, 200), - null /* boundingRects */); + null /* boundingRects */, + 0 /* flags */); mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); final WindowContainerTransaction wct2 = new WindowContainerTransaction(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTracingTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTracingLegacyTest.java index c1834037f791..48a8d5502c64 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTracingTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTracingLegacyTest.java @@ -63,7 +63,7 @@ import java.nio.charset.StandardCharsets; */ @SmallTest @Presubmit -public class WindowTracingTest { +public class WindowTracingLegacyTest { private static final byte[] MAGIC_HEADER = new byte[]{ 0x9, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45, @@ -88,7 +88,7 @@ public class WindowTracingTest { mFile = testContext.getFileStreamPath("tracing_test.dat"); mFile.delete(); - mWindowTracing = new WindowTracing(mFile, mWmMock, mChoreographer, + mWindowTracing = new WindowTracingLegacy(mFile, mWmMock, mChoreographer, new WindowManagerGlobalLock(), 1024); } diff --git a/telecomm/java/android/telecom/CallAudioState.java b/telecomm/java/android/telecom/CallAudioState.java index 49e9232ad535..14c9ea51c618 100644 --- a/telecomm/java/android/telecom/CallAudioState.java +++ b/telecomm/java/android/telecom/CallAudioState.java @@ -159,7 +159,7 @@ public final class CallAudioState implements Parcelable { @Override public String toString() { String bluetoothDeviceList = supportedBluetoothDevices.stream() - .map(BluetoothDevice::getAddress).collect(Collectors.joining(", ")); + .map(BluetoothDevice::toString).collect(Collectors.joining(", ")); return String.format(Locale.US, "[AudioState isMuted: %b, route: %s, supportedRouteMask: %s, " + diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index b518c60d09fd..aebae4eec3f1 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -50,7 +50,6 @@ import java.lang.annotation.RetentionPolicy; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -2636,9 +2635,9 @@ public final class SatelliteManager { if (resultCode == SATELLITE_RESULT_SUCCESS) { if (resultData.containsKey(KEY_REQUEST_PROVISION_SUBSCRIBER_ID_TOKEN)) { List<ProvisionSubscriberId> list = - Collections.singletonList(resultData.getParcelable( + resultData.getParcelableArrayList( KEY_REQUEST_PROVISION_SUBSCRIBER_ID_TOKEN, - ProvisionSubscriberId.class)); + ProvisionSubscriberId.class); executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onResult(list))); } else { @@ -2692,13 +2691,13 @@ public final class SatelliteManager { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { if (resultCode == SATELLITE_RESULT_SUCCESS) { - if (resultData.containsKey(KEY_SATELLITE_PROVISIONED)) { + if (resultData.containsKey(KEY_IS_SATELLITE_PROVISIONED)) { boolean isIsProvisioned = - resultData.getBoolean(KEY_SATELLITE_PROVISIONED); + resultData.getBoolean(KEY_IS_SATELLITE_PROVISIONED); executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onResult(isIsProvisioned))); } else { - loge("KEY_REQUEST_PROVISION_TOKENS does not exist."); + loge("KEY_IS_SATELLITE_PROVISIONED does not exist."); executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(new SatelliteException( SATELLITE_RESULT_REQUEST_FAILED)))); diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 7be52ea62758..3dbda7ae10a3 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3411,7 +3411,7 @@ interface ITelephony { */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") - void requestProvisionSubscriberIds(in ResultReceiver receiver); + void requestProvisionSubscriberIds(in ResultReceiver result); /** * Request to get provisioned status for given a satellite subscriber id. diff --git a/tests/FlickerTests/IME/Android.bp b/tests/FlickerTests/IME/Android.bp index ccc3683f0b93..78d93e1cb32a 100644 --- a/tests/FlickerTests/IME/Android.bp +++ b/tests/FlickerTests/IME/Android.bp @@ -34,6 +34,11 @@ filegroup { srcs: ["src/**/Close*"], } +filegroup { + name: "FlickerTestsIme2-src", + srcs: ["src/**/ShowImeOnAppStart*"], +} + android_test { name: "FlickerTestsIme", defaults: ["FlickerTestsDefault"], @@ -77,9 +82,23 @@ android_test { defaults: ["FlickerTestsDefault"], manifest: "AndroidManifest.xml", test_config_template: "AndroidTestTemplate.xml", + srcs: [":FlickerTestsIme2-src"], + static_libs: [ + "FlickerTestsBase", + "FlickerTestsImeCommon", + ], + data: ["trace_config/*"], +} + +android_test { + name: "FlickerTestsIme3", + defaults: ["FlickerTestsDefault"], + manifest: "AndroidManifest.xml", + test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], exclude_srcs: [ ":FlickerTestsIme1-src", + ":FlickerTestsIme2-src", ":FlickerTestsImeCommon-src", ], static_libs: [ diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 9a5e88becf1e..238f2afa7994 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -165,4 +165,37 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : Corners.RIGHT_BOTTOM -> Pair(windowRect.right, windowRect.bottom) } } + + /** Exit desktop mode by dragging the app handle to the top drag zone. */ + fun exitDesktopWithDragToTopDragZone( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + ) { + dragAppWindowToTopDragZone(wmHelper, device) + waitForTransitionToFullscreen(wmHelper) + } + + private fun dragAppWindowToTopDragZone(wmHelper: WindowManagerStateHelper, device: UiDevice) { + val windowRect = wmHelper.getWindowRegion(innerHelper).bounds + val displayRect = + wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect + ?: throw IllegalStateException("Default display is null") + + val startX = windowRect.centerX() + val endX = displayRect.centerX() + val startY = windowRect.top + val endY = 0 // top of the screen + + // drag the app window to top drag zone + device.drag(startX, startY, endX, endY, 100) + } + + /** Wait for transition to full screen to finish. */ + private fun waitForTransitionToFullscreen(wmHelper: WindowManagerStateHelper) { + wmHelper + .StateSyncBuilder() + .withFullScreenApp(innerHelper) + .withAppTransitionIdle() + .waitForAndVerify() + } } diff --git a/tests/Input/AndroidTest.xml b/tests/Input/AndroidTest.xml index 8db37058af2b..4a99bd4f1801 100644 --- a/tests/Input/AndroidTest.xml +++ b/tests/Input/AndroidTest.xml @@ -31,7 +31,7 @@ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> <option name="pull-pattern-keys" value="input_.*" /> <!-- Pull files created by tests, like the output of screenshot tests --> - <option name="directory-keys" value="/storage/emulated/0/InputTests" /> + <option name="directory-keys" value="/sdcard/Download/InputTests" /> <option name="collect-on-run-ended-only" value="false" /> </metrics_collector> </configuration> diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt index e0f8c6d6ff4a..d0148fbbee3d 100644 --- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt +++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt @@ -19,7 +19,6 @@ package com.android.test.input import android.content.Context import android.content.res.Configuration import android.content.res.Resources -import android.os.Environment import android.view.ContextThemeWrapper import android.view.PointerIcon import android.view.flags.Flags.enableVectorCursorA11ySettings @@ -158,8 +157,7 @@ class PointerIconLoadingTest { const val SCREEN_WIDTH_DP = 480 const val SCREEN_HEIGHT_DP = 800 const val ASSETS_PATH = "tests/input/assets" - val TEST_OUTPUT_PATH = Environment.getExternalStorageDirectory().absolutePath + - "/InputTests/" + - PointerIconLoadingTest::class.java.simpleName + val TEST_OUTPUT_PATH = + "/sdcard/Download/InputTests/" + PointerIconLoadingTest::class.java.simpleName } } diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java index df2374572296..f3851790688e 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java @@ -71,7 +71,6 @@ public final class ConcurrentMultiUserTest { private static final ComponentName TEST_ACTIVITY = new ComponentName( getInstrumentation().getTargetContext().getPackageName(), MainActivity.class.getName()); - private static final long WAIT_TIME_MS = 3000L; private final Context mContext = getInstrumentation().getTargetContext(); private final InputMethodManager mInputMethodManager = mContext.getSystemService(InputMethodManager.class); @@ -257,8 +256,6 @@ public final class ConcurrentMultiUserTest { float[] driverEditTextCenter = mActivity.getEditTextCenter(); SystemUtil.runShellCommand(mUiAutomation, String.format("input tap %f %f", driverEditTextCenter[0], driverEditTextCenter[1])); - // TODO(b/350562427): get rid of Thread.sleep(). - Thread.sleep(WAIT_TIME_MS); } private void movePassengerDisplayToTop() throws Exception { @@ -274,8 +271,6 @@ public final class ConcurrentMultiUserTest { final int passengerDisplayId = receivedBundle.getInt(KEY_DISPLAY_ID); SystemUtil.runShellCommand(mUiAutomation, String.format("input -d %d tap %f %f", passengerDisplayId, passengerEditTextCenter[0], passengerEditTextCenter[1])); - // TODO(b/350562427): get rid of Thread.sleep(). - Thread.sleep(WAIT_TIME_MS); } /** diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index ee53af107b17..012b0f230ca2 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -116,6 +116,10 @@ class OptimizeCommand : public Command { "This decreases APK size at the cost of resource retrieval performance.\n" "Applies sparse encoding to all resources regardless of minSdk.", &options_.force_sparse_encoding); + AddOptionalSwitch( + "--enable-compact-entries", + "This decreases APK size by using compact resource entries for simple data types.", + &options_.table_flattener_options.use_compact_entries); AddOptionalSwitch("--collapse-resource-names", "Collapses resource names to a single value in the key string pool. Resources can \n" "be exempted using the \"no_collapse\" directive in a file specified by " diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp index 669cddb9af5a..cbf2c2fe8a9c 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -18,9 +18,9 @@ #include <unordered_set> -#include "android-base/logging.h" - #include "ResourceUtils.h" +#include "android-base/logging.h" +#include "process/SymbolTable.h" #include "trace/TraceBuffer.h" #include "util/Util.h" #include "xml/XmlActionExecutor.h" @@ -172,6 +172,58 @@ static bool RequiredNameIsJavaClassName(xml::Element* el, android::SourcePathDia return NameIsJavaClassName(el, attr, diag); } +static bool UpdateConfigChangesIfNeeded(xml::Element* el, IAaptContext* context) { + xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "configChanges"); + if (attr == nullptr) { + return true; + } + + if (attr->value != "allKnown" && attr->value.find("allKnown") != std::string::npos) { + context->GetDiagnostics()->Error( + android::DiagMessage(el->line_number) + << "If you want to declare 'allKnown' in attribute 'android:configChanges' in <" << el->name + << ">, " << attr->value << " is not allowed', allKnown has to be used " + << "by itself, for example: 'android:configChanges=allKnown', it cannot be combined with " + << "the other flags"); + return false; + } + + if (attr->value == "allKnown") { + SymbolTable* symbol_table = context->GetExternalSymbols(); + const SymbolTable::Symbol* symbol = + symbol_table->FindByName(ResourceName("android", ResourceType::kAttr, "configChanges")); + + if (symbol == nullptr) { + context->GetDiagnostics()->Error( + android::DiagMessage(el->line_number) + << "Cannot find symbol for android:configChanges with min sdk: " + << context->GetMinSdkVersion()); + } + + std::stringstream new_value; + + const auto& symbols = symbol->attribute->symbols; + for (auto it = symbols.begin(); it != symbols.end(); ++it) { + // Skip 'resourcesUnused' which is the flag to fully disable activity restart specifically + // for games. + if (it->symbol.name.value().entry == "resourcesUnused") { + continue; + } + if (it != symbols.begin()) { + new_value << "|"; + } + new_value << it->symbol.name.value().entry; + } + const auto& old_value = attr->value; + auto new_value_str = new_value.str(); + context->GetDiagnostics()->Note(android::DiagMessage(el->line_number) + << "Updating value of 'android:configChanges' from " + << old_value << " to " << new_value_str); + attr->value = std::move(new_value_str); + } + return true; +} + static bool RequiredNameIsJavaPackage(xml::Element* el, android::SourcePathDiagnostics* diag) { xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "name"); if (attr == nullptr) { @@ -382,8 +434,9 @@ static void EnsureNamespaceIsDeclared(const std::string& prefix, const std::stri } } -bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagnostics* diag) { +bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, IAaptContext* context) { // First verify some options. + android::IDiagnostics* diag = context->GetDiagnostics(); if (options_.rename_manifest_package) { if (!util::IsJavaPackageName(options_.rename_manifest_package.value())) { diag->Error(android::DiagMessage() << "invalid manifest package override '" @@ -432,6 +485,8 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagn // Common component actions. xml::XmlNodeAction component_action; component_action.Action(RequiredNameIsJavaClassName); + component_action.Action( + [context](xml::Element* el) -> bool { return UpdateConfigChangesIfNeeded(el, context); }); component_action["intent-filter"] = intent_filter_action; component_action["preferred"] = intent_filter_action; component_action["meta-data"] = meta_data_action; @@ -778,7 +833,7 @@ bool ManifestFixer::Consume(IAaptContext* context, xml::XmlResource* doc) { } xml::XmlActionExecutor executor; - if (!BuildRules(&executor, context->GetDiagnostics())) { + if (!BuildRules(&executor, context)) { return false; } diff --git a/tools/aapt2/link/ManifestFixer.h b/tools/aapt2/link/ManifestFixer.h index df0ece6fe24a..748a82858175 100644 --- a/tools/aapt2/link/ManifestFixer.h +++ b/tools/aapt2/link/ManifestFixer.h @@ -110,7 +110,7 @@ class ManifestFixer : public IXmlResourceConsumer { private: DISALLOW_COPY_AND_ASSIGN(ManifestFixer); - bool BuildRules(xml::XmlActionExecutor* executor, android::IDiagnostics* diag); + bool BuildRules(xml::XmlActionExecutor* executor, IAaptContext* context); ManifestFixerOptions options_; }; diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp index 3cfdf7832801..50086273a819 100644 --- a/tools/aapt2/link/ManifestFixer_test.cpp +++ b/tools/aapt2/link/ManifestFixer_test.cpp @@ -37,27 +37,30 @@ struct ManifestFixerTest : public ::testing::Test { .SetCompilationPackage("android") .SetPackageId(0x01) .SetNameManglerPolicy(NameManglerPolicy{"android"}) - .AddSymbolSource( - test::StaticSymbolSourceBuilder() - .AddSymbol( - "android:attr/package", ResourceId(0x01010000), - test::AttributeBuilder() - .SetTypeMask(android::ResTable_map::TYPE_STRING) - .Build()) - .AddSymbol( - "android:attr/minSdkVersion", ResourceId(0x01010001), - test::AttributeBuilder() - .SetTypeMask(android::ResTable_map::TYPE_STRING | - android::ResTable_map::TYPE_INTEGER) - .Build()) - .AddSymbol( - "android:attr/targetSdkVersion", ResourceId(0x01010002), - test::AttributeBuilder() - .SetTypeMask(android::ResTable_map::TYPE_STRING | - android::ResTable_map::TYPE_INTEGER) - .Build()) - .AddSymbol("android:string/str", ResourceId(0x01060000)) - .Build()) + .AddSymbolSource(test::StaticSymbolSourceBuilder() + .AddSymbol("android:attr/package", ResourceId(0x01010000), + test::AttributeBuilder() + .SetTypeMask(android::ResTable_map::TYPE_STRING) + .Build()) + .AddSymbol("android:attr/minSdkVersion", ResourceId(0x01010001), + test::AttributeBuilder() + .SetTypeMask(android::ResTable_map::TYPE_STRING | + android::ResTable_map::TYPE_INTEGER) + .Build()) + .AddSymbol("android:attr/targetSdkVersion", ResourceId(0x01010002), + test::AttributeBuilder() + .SetTypeMask(android::ResTable_map::TYPE_STRING | + android::ResTable_map::TYPE_INTEGER) + .Build()) + .AddSymbol("android:string/str", ResourceId(0x01060000)) + .AddSymbol("android:attr/configChanges", ResourceId(0x01010003), + test::AttributeBuilder() + .AddItem("testConfigChange1", 1) + .AddItem("testConfigChange2", 2) + .AddItem("resourcesUnused", 4) + .SetTypeMask(android::ResTable_map::TYPE_STRING) + .Build()) + .Build()) .Build(); } @@ -1591,4 +1594,72 @@ TEST_F(ManifestFixerTest, IntentFilterPathMustStartWithLeadingSlashOnDeepLinks) </manifest>)"; EXPECT_THAT(Verify(input), NotNull()); } + +TEST_F(ManifestFixerTest, AllKnownNotDeclaredProperly) { + std::string input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity" + android:configChanges="allKnown|testConfigChange1"> + </activity> + </application> + </manifest>)"; + auto doc = Verify(input); + EXPECT_THAT(doc, IsNull()); +} + +TEST_F(ManifestFixerTest, ModifyAttributeValueForAllKnown) { + std::string input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity" + android:configChanges="allKnown"> + </activity> + </application> + </manifest>)"; + auto doc = Verify(input); + EXPECT_THAT(doc, NotNull()); + + xml::Element* el; + xml::Attribute* attr; + + el = doc->root.get(); + ASSERT_THAT(el, NotNull()); + el = el->FindChild({}, "application"); + ASSERT_THAT(el, NotNull()); + el = el->FindChild({}, "activity"); + ASSERT_THAT(el, NotNull()); + + attr = el->FindAttribute(xml::kSchemaAndroid, "configChanges"); + ASSERT_THAT(attr->value, "testConfigChange1|testConfigChange2"); +} + +TEST_F(ManifestFixerTest, DoNothingForOtherConfigChanges) { + std::string input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity" + android:configChanges="testConfigChange2"> + </activity> + </application> + </manifest>)"; + auto doc = Verify(input); + EXPECT_THAT(doc, NotNull()); + + xml::Element* el; + xml::Attribute* attr; + + el = doc->root.get(); + ASSERT_THAT(el, NotNull()); + el = el->FindChild({}, "application"); + ASSERT_THAT(el, NotNull()); + el = el->FindChild({}, "activity"); + ASSERT_THAT(el, NotNull()); + + attr = el->FindAttribute(xml::kSchemaAndroid, "configChanges"); + ASSERT_THAT(attr->value, "testConfigChange2"); +} } // namespace aapt diff --git a/tools/aapt2/util/Files.cpp b/tools/aapt2/util/Files.cpp index 02e4beaed949..8ae55b8868c3 100644 --- a/tools/aapt2/util/Files.cpp +++ b/tools/aapt2/util/Files.cpp @@ -189,7 +189,7 @@ void AppendPath(std::string* base, StringPiece part) { base->append(part.data(), part.size()); } -std::string BuildPath(std::vector<const StringPiece>&& args) { +std::string BuildPath(const std::vector<StringPiece>& args) { if (args.empty()) { return ""; } diff --git a/tools/aapt2/util/Files.h b/tools/aapt2/util/Files.h index 42eeaf2d2e2a..c1a42446ec3b 100644 --- a/tools/aapt2/util/Files.h +++ b/tools/aapt2/util/Files.h @@ -60,7 +60,7 @@ FileType GetFileType(const std::string& path); void AppendPath(std::string* base, android::StringPiece part); // Concatenates the list of paths and separates each part with the directory separator. -std::string BuildPath(std::vector<const android::StringPiece>&& args); +std::string BuildPath(const std::vector<android::StringPiece>& args); // Makes all the directories in `path`. The last element in the path is interpreted as a directory. bool mkdirs(const std::string& path); diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java index 191f38d3df80..718d8987c13b 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java @@ -62,13 +62,11 @@ public class AslConverter { XmlUtils.getSingleChildElement( document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES, true); - return new AndroidSafetyLabelFactory() - .createFromHrElements(XmlUtils.listOf(appMetadataBundles)); + return new AndroidSafetyLabelFactory().createFromHrElement(appMetadataBundles); case ON_DEVICE: Element bundleEle = XmlUtils.getSingleChildElement(document, XmlUtils.OD_TAG_BUNDLE, true); - return new AndroidSafetyLabelFactory() - .createFromOdElements(XmlUtils.listOf(bundleEle)); + return new AndroidSafetyLabelFactory().createFromOdElement(bundleEle); default: throw new IllegalStateException("Unrecognized input format."); } @@ -91,14 +89,10 @@ public class AslConverter { switch (format) { case HUMAN_READABLE: - for (var child : asl.toHrDomElements(document)) { - document.appendChild(child); - } + document.appendChild(asl.toHrDomElement(document)); break; case ON_DEVICE: - for (var child : asl.toOdDomElements(document)) { - document.appendChild(child); - } + document.appendChild(asl.toOdDomElement(document)); break; default: throw new IllegalStateException("Unrecognized input format."); diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java index 72140a17297c..8b2fd93df889 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java @@ -21,8 +21,6 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; -import java.util.List; - public class AndroidSafetyLabel implements AslMarshallable { private final Long mVersion; @@ -46,36 +44,34 @@ public class AndroidSafetyLabel implements AslMarshallable { } /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */ - @Override - public List<Element> toOdDomElements(Document doc) { + public Element toOdDomElement(Document doc) { Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE); aslEle.appendChild(XmlUtils.createOdLongEle(doc, XmlUtils.OD_NAME_VERSION, mVersion)); if (mSafetyLabels != null) { - XmlUtils.appendChildren(aslEle, mSafetyLabels.toOdDomElements(doc)); + aslEle.appendChild(mSafetyLabels.toOdDomElement(doc)); } if (mSystemAppSafetyLabel != null) { - XmlUtils.appendChildren(aslEle, mSystemAppSafetyLabel.toOdDomElements(doc)); + aslEle.appendChild(mSystemAppSafetyLabel.toOdDomElement(doc)); } if (mTransparencyInfo != null) { - XmlUtils.appendChildren(aslEle, mTransparencyInfo.toOdDomElements(doc)); + aslEle.appendChild(mTransparencyInfo.toOdDomElement(doc)); } - return XmlUtils.listOf(aslEle); + return aslEle; } /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - @Override - public List<Element> toHrDomElements(Document doc) { + public Element toHrDomElement(Document doc) { Element aslEle = doc.createElement(XmlUtils.HR_TAG_APP_METADATA_BUNDLES); aslEle.setAttribute(XmlUtils.HR_ATTR_VERSION, String.valueOf(mVersion)); if (mSafetyLabels != null) { - XmlUtils.appendChildren(aslEle, mSafetyLabels.toHrDomElements(doc)); + aslEle.appendChild(mSafetyLabels.toHrDomElement(doc)); } if (mSystemAppSafetyLabel != null) { - XmlUtils.appendChildren(aslEle, mSystemAppSafetyLabel.toHrDomElements(doc)); + aslEle.appendChild(mSystemAppSafetyLabel.toHrDomElement(doc)); } if (mTransparencyInfo != null) { - XmlUtils.appendChildren(aslEle, mTransparencyInfo.toHrDomElements(doc)); + aslEle.appendChild(mTransparencyInfo.toHrDomElement(doc)); } - return XmlUtils.listOf(aslEle); + return aslEle; } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java index c53cbbf99a46..b9eb2a35e63b 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java @@ -21,65 +21,117 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; -import java.util.List; +import java.util.Map; +import java.util.Set; public class AndroidSafetyLabelFactory implements AslMarshallableFactory<AndroidSafetyLabel> { + private final Map<Long, Set<String>> mRecognizedHrAttrs = + Map.ofEntries(Map.entry(1L, Set.of(XmlUtils.HR_ATTR_VERSION))); + private final Map<Long, Set<String>> mRequiredHrAttrs = + Map.ofEntries(Map.entry(1L, Set.of(XmlUtils.HR_ATTR_VERSION))); + private final Map<Long, Set<String>> mRecognizedHrEles = + Map.ofEntries( + Map.entry( + 1L, + Set.of( + XmlUtils.HR_TAG_SYSTEM_APP_SAFETY_LABEL, + XmlUtils.HR_TAG_SAFETY_LABELS, + XmlUtils.HR_TAG_TRANSPARENCY_INFO))); + private final Map<Long, Set<String>> mRequiredHrEles = + Map.ofEntries( + Map.entry(1L, Set.of()), + Map.entry( + 2L, + Set.of( + XmlUtils.HR_TAG_SYSTEM_APP_SAFETY_LABEL, + XmlUtils.HR_TAG_TRANSPARENCY_INFO))); + private final Map<Long, Set<String>> mRecognizedOdEleNames = + Map.ofEntries( + Map.entry( + 1L, + Set.of( + XmlUtils.OD_NAME_VERSION, + XmlUtils.OD_NAME_SAFETY_LABELS, + XmlUtils.OD_NAME_SYSTEM_APP_SAFETY_LABEL, + XmlUtils.OD_NAME_TRANSPARENCY_INFO))); + private final Map<Long, Set<String>> mRequiredOdEleNames = + Map.ofEntries( + Map.entry(1L, Set.of(XmlUtils.OD_NAME_VERSION)), + Map.entry( + 2L, + Set.of( + XmlUtils.OD_NAME_VERSION, + XmlUtils.OD_NAME_SYSTEM_APP_SAFETY_LABEL, + XmlUtils.OD_NAME_TRANSPARENCY_INFO))); /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */ - @Override - public AndroidSafetyLabel createFromHrElements(List<Element> appMetadataBundles) + public AndroidSafetyLabel createFromHrElement(Element appMetadataBundlesEle) throws MalformedXmlException { - Element appMetadataBundlesEle = XmlUtils.getSingleElement(appMetadataBundles); long version = XmlUtils.tryGetVersion(appMetadataBundlesEle); + XmlUtils.throwIfExtraneousAttributes( + appMetadataBundlesEle, XmlUtils.getMostRecentVersion(mRecognizedHrAttrs, version)); + XmlUtils.throwIfExtraneousChildrenHr( + appMetadataBundlesEle, XmlUtils.getMostRecentVersion(mRecognizedHrEles, version)); Element safetyLabelsEle = XmlUtils.getSingleChildElement( - appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS, false); + appMetadataBundlesEle, + XmlUtils.HR_TAG_SAFETY_LABELS, + XmlUtils.getMostRecentVersion(mRequiredHrEles, version)); SafetyLabels safetyLabels = - new SafetyLabelsFactory().createFromHrElements(XmlUtils.listOf(safetyLabelsEle)); + new SafetyLabelsFactory().createFromHrElement(safetyLabelsEle, version); Element systemAppSafetyLabelEle = XmlUtils.getSingleChildElement( - appMetadataBundlesEle, XmlUtils.HR_TAG_SYSTEM_APP_SAFETY_LABEL, false); + appMetadataBundlesEle, + XmlUtils.HR_TAG_SYSTEM_APP_SAFETY_LABEL, + XmlUtils.getMostRecentVersion(mRequiredHrEles, version)); SystemAppSafetyLabel systemAppSafetyLabel = new SystemAppSafetyLabelFactory() - .createFromHrElements(XmlUtils.listOf(systemAppSafetyLabelEle)); + .createFromHrElement(systemAppSafetyLabelEle, version); Element transparencyInfoEle = XmlUtils.getSingleChildElement( - appMetadataBundlesEle, XmlUtils.HR_TAG_TRANSPARENCY_INFO, false); + appMetadataBundlesEle, + XmlUtils.HR_TAG_TRANSPARENCY_INFO, + XmlUtils.getMostRecentVersion(mRequiredHrEles, version)); TransparencyInfo transparencyInfo = - new TransparencyInfoFactory() - .createFromHrElements(XmlUtils.listOf(transparencyInfoEle)); + new TransparencyInfoFactory().createFromHrElement(transparencyInfoEle, version); return new AndroidSafetyLabel( version, systemAppSafetyLabel, safetyLabels, transparencyInfo); } /** Creates an {@link AndroidSafetyLabel} from on-device DOM elements */ - @Override - public AndroidSafetyLabel createFromOdElements(List<Element> elements) - throws MalformedXmlException { - Element bundleEle = XmlUtils.getSingleElement(elements); + public AndroidSafetyLabel createFromOdElement(Element bundleEle) throws MalformedXmlException { Long version = XmlUtils.getOdLongEle(bundleEle, XmlUtils.OD_NAME_VERSION, true); + XmlUtils.throwIfExtraneousChildrenOd( + bundleEle, XmlUtils.getMostRecentVersion(mRecognizedOdEleNames, version)); Element safetyLabelsEle = - XmlUtils.getOdPbundleWithName(bundleEle, XmlUtils.OD_NAME_SAFETY_LABELS, false); + XmlUtils.getOdPbundleWithName( + bundleEle, + XmlUtils.OD_NAME_SAFETY_LABELS, + XmlUtils.getMostRecentVersion(mRequiredOdEleNames, version)); SafetyLabels safetyLabels = - new SafetyLabelsFactory().createFromOdElements(XmlUtils.listOf(safetyLabelsEle)); + new SafetyLabelsFactory().createFromOdElement(safetyLabelsEle, version); Element systemAppSafetyLabelEle = XmlUtils.getOdPbundleWithName( - bundleEle, XmlUtils.OD_NAME_SYSTEM_APP_SAFETY_LABEL, false); + bundleEle, + XmlUtils.OD_NAME_SYSTEM_APP_SAFETY_LABEL, + XmlUtils.getMostRecentVersion(mRequiredOdEleNames, version)); SystemAppSafetyLabel systemAppSafetyLabel = new SystemAppSafetyLabelFactory() - .createFromOdElements(XmlUtils.listOf(systemAppSafetyLabelEle)); + .createFromOdElement(systemAppSafetyLabelEle, version); Element transparencyInfoEle = - XmlUtils.getOdPbundleWithName(bundleEle, XmlUtils.OD_NAME_TRANSPARENCY_INFO, false); + XmlUtils.getOdPbundleWithName( + bundleEle, + XmlUtils.OD_NAME_TRANSPARENCY_INFO, + XmlUtils.getMostRecentVersion(mRequiredOdEleNames, version)); TransparencyInfo transparencyInfo = - new TransparencyInfoFactory() - .createFromOdElements(XmlUtils.listOf(transparencyInfoEle)); + new TransparencyInfoFactory().createFromOdElement(transparencyInfoEle, version); return new AndroidSafetyLabel( version, systemAppSafetyLabel, safetyLabels, transparencyInfo); diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java index f39722589a96..d2557aec5f82 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java @@ -25,36 +25,107 @@ import java.util.List; /** AppInfo representation */ public class AppInfo implements AslMarshallable { - private final Boolean mApsCompliant; + private final String mTitle; + private final String mDescription; + private final Boolean mContainsAds; + private final Boolean mObeyAps; + private final Boolean mAdsFingerprinting; + private final Boolean mSecurityFingerprinting; private final String mPrivacyPolicy; + private final List<String> mSecurityEndpoints; private final List<String> mFirstPartyEndpoints; private final List<String> mServiceProviderEndpoints; + private final String mCategory; + private final String mEmail; + private final String mWebsite; + + private final Boolean mApsCompliant; + private final String mDeveloperId; + private final String mApplicationId; + + // private final String mPrivacyPolicy; + // private final List<String> mFirstPartyEndpoints; + // private final List<String> mServiceProviderEndpoints; public AppInfo( - Boolean apsCompliant, + String title, + String description, + Boolean containsAds, + Boolean obeyAps, + Boolean adsFingerprinting, + Boolean securityFingerprinting, String privacyPolicy, + List<String> securityEndpoints, List<String> firstPartyEndpoints, - List<String> serviceProviderEndpoints) { - this.mApsCompliant = apsCompliant; + List<String> serviceProviderEndpoints, + String category, + String email, + String website, + Boolean apsCompliant, + String developerId, + String applicationId) { + this.mTitle = title; + this.mDescription = description; + this.mContainsAds = containsAds; + this.mObeyAps = obeyAps; + this.mAdsFingerprinting = adsFingerprinting; + this.mSecurityFingerprinting = securityFingerprinting; this.mPrivacyPolicy = privacyPolicy; + this.mSecurityEndpoints = securityEndpoints; this.mFirstPartyEndpoints = firstPartyEndpoints; this.mServiceProviderEndpoints = serviceProviderEndpoints; + this.mCategory = category; + this.mEmail = email; + this.mWebsite = website; + this.mApsCompliant = apsCompliant; + this.mDeveloperId = developerId; + this.mApplicationId = applicationId; } /** Creates an on-device DOM element from the {@link SafetyLabels}. */ - @Override - public List<Element> toOdDomElements(Document doc) { + public Element toOdDomElement(Document doc) { Element appInfoEle = XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_APP_INFO); - if (this.mApsCompliant != null) { + + if (this.mTitle != null) { + appInfoEle.appendChild(XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_TITLE, mTitle)); + } + if (this.mDescription != null) { + appInfoEle.appendChild( + XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_DESCRIPTION, mDescription)); + } + if (this.mContainsAds != null) { + appInfoEle.appendChild( + XmlUtils.createOdBooleanEle(doc, XmlUtils.OD_NAME_CONTAINS_ADS, mContainsAds)); + } + if (this.mObeyAps != null) { + appInfoEle.appendChild( + XmlUtils.createOdBooleanEle(doc, XmlUtils.OD_NAME_OBEY_APS, mObeyAps)); + } + if (this.mAdsFingerprinting != null) { appInfoEle.appendChild( XmlUtils.createOdBooleanEle( - doc, XmlUtils.OD_NAME_APS_COMPLIANT, mApsCompliant)); + doc, XmlUtils.OD_NAME_ADS_FINGERPRINTING, mAdsFingerprinting)); + } + if (this.mSecurityFingerprinting != null) { + appInfoEle.appendChild( + XmlUtils.createOdBooleanEle( + doc, + XmlUtils.OD_NAME_SECURITY_FINGERPRINTING, + mSecurityFingerprinting)); } if (this.mPrivacyPolicy != null) { appInfoEle.appendChild( XmlUtils.createOdStringEle( doc, XmlUtils.OD_NAME_PRIVACY_POLICY, mPrivacyPolicy)); } + if (this.mSecurityEndpoints != null) { + appInfoEle.appendChild( + XmlUtils.createOdArray( + doc, + XmlUtils.OD_TAG_STRING_ARRAY, + XmlUtils.OD_NAME_SECURITY_ENDPOINTS, + mSecurityEndpoints)); + } if (this.mFirstPartyEndpoints != null) { appInfoEle.appendChild( XmlUtils.createOdArray( @@ -71,27 +142,88 @@ public class AppInfo implements AslMarshallable { XmlUtils.OD_NAME_SERVICE_PROVIDER_ENDPOINTS, mServiceProviderEndpoints)); } - return XmlUtils.listOf(appInfoEle); + if (this.mCategory != null) { + appInfoEle.appendChild( + XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_CATEGORY, this.mCategory)); + } + if (this.mEmail != null) { + appInfoEle.appendChild( + XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_EMAIL, this.mEmail)); + } + if (this.mWebsite != null) { + appInfoEle.appendChild( + XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_WEBSITE, this.mWebsite)); + } + + if (this.mApsCompliant != null) { + appInfoEle.appendChild( + XmlUtils.createOdBooleanEle( + doc, XmlUtils.OD_NAME_APS_COMPLIANT, mApsCompliant)); + } + if (this.mDeveloperId != null) { + appInfoEle.appendChild( + XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_DEVELOPER_ID, mDeveloperId)); + } + if (this.mApplicationId != null) { + appInfoEle.appendChild( + XmlUtils.createOdStringEle( + doc, XmlUtils.OD_NAME_APPLICATION_ID, mApplicationId)); + } + return appInfoEle; } /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - @Override - public List<Element> toHrDomElements(Document doc) { + public Element toHrDomElement(Document doc) { Element appInfoEle = doc.createElement(XmlUtils.HR_TAG_APP_INFO); - if (this.mApsCompliant != null) { + + if (this.mTitle != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_TITLE, this.mTitle); + } + if (this.mDescription != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_DESCRIPTION, this.mDescription); + } + if (this.mContainsAds != null) { appInfoEle.setAttribute( - XmlUtils.HR_ATTR_APS_COMPLIANT, String.valueOf(this.mApsCompliant)); + XmlUtils.HR_ATTR_CONTAINS_ADS, String.valueOf(this.mContainsAds)); + } + if (this.mObeyAps != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_OBEY_APS, String.valueOf(this.mObeyAps)); + } + if (this.mAdsFingerprinting != null) { + appInfoEle.setAttribute( + XmlUtils.HR_ATTR_ADS_FINGERPRINTING, String.valueOf(this.mAdsFingerprinting)); + } + if (this.mSecurityFingerprinting != null) { + appInfoEle.setAttribute( + XmlUtils.HR_ATTR_SECURITY_FINGERPRINTING, + String.valueOf(this.mSecurityFingerprinting)); } if (this.mPrivacyPolicy != null) { appInfoEle.setAttribute(XmlUtils.HR_ATTR_PRIVACY_POLICY, this.mPrivacyPolicy); } + if (this.mSecurityEndpoints != null) { + appInfoEle.setAttribute( + XmlUtils.HR_ATTR_SECURITY_ENDPOINTS, String.join("|", this.mSecurityEndpoints)); + } + if (this.mCategory != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_CATEGORY, this.mCategory); + } + if (this.mEmail != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_EMAIL, this.mEmail); + } + if (this.mWebsite != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_WEBSITE, this.mWebsite); + } + if (this.mApsCompliant != null) { + appInfoEle.setAttribute( + XmlUtils.HR_ATTR_APS_COMPLIANT, String.valueOf(this.mApsCompliant)); + } if (this.mFirstPartyEndpoints != null) { appInfoEle.appendChild( XmlUtils.createHrArray( doc, XmlUtils.HR_TAG_FIRST_PARTY_ENDPOINTS, mFirstPartyEndpoints)); } - if (this.mServiceProviderEndpoints != null) { appInfoEle.appendChild( XmlUtils.createHrArray( @@ -99,7 +231,13 @@ public class AppInfo implements AslMarshallable { XmlUtils.HR_TAG_SERVICE_PROVIDER_ENDPOINTS, mServiceProviderEndpoints)); } + if (this.mDeveloperId != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_DEVELOPER_ID, this.mDeveloperId); + } + if (this.mApplicationId != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_APPLICATION_ID, this.mApplicationId); + } - return XmlUtils.listOf(appInfoEle); + return appInfoEle; } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java index 6ad202765218..277a508ced57 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java @@ -16,60 +16,233 @@ package com.android.asllib.marshallable; -import com.android.asllib.util.AslgenUtil; import com.android.asllib.util.MalformedXmlException; import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; import java.util.List; +import java.util.Map; +import java.util.Set; public class AppInfoFactory implements AslMarshallableFactory<AppInfo> { + // We don't need to support V1 for HR. + private final Map<Long, Set<String>> mRecognizedHrAttrs = + Map.ofEntries( + Map.entry( + 2L, + Set.of( + XmlUtils.HR_ATTR_APS_COMPLIANT, + XmlUtils.HR_ATTR_PRIVACY_POLICY, + XmlUtils.HR_ATTR_DEVELOPER_ID, + XmlUtils.HR_ATTR_APPLICATION_ID))); + private final Map<Long, Set<String>> mRequiredHrAttrs = + Map.ofEntries( + Map.entry( + 2L, + Set.of( + XmlUtils.HR_ATTR_APS_COMPLIANT, + XmlUtils.HR_ATTR_PRIVACY_POLICY, + XmlUtils.HR_ATTR_DEVELOPER_ID, + XmlUtils.HR_ATTR_APPLICATION_ID))); + private final Map<Long, Set<String>> mRecognizedHrEles = + Map.ofEntries( + Map.entry( + 2L, + Set.of( + XmlUtils.HR_TAG_FIRST_PARTY_ENDPOINTS, + XmlUtils.HR_TAG_SERVICE_PROVIDER_ENDPOINTS))); + private final Map<Long, Set<String>> mRequiredHrEles = + Map.ofEntries( + Map.entry( + 2L, + Set.of( + XmlUtils.HR_TAG_FIRST_PARTY_ENDPOINTS, + XmlUtils.HR_TAG_SERVICE_PROVIDER_ENDPOINTS))); + private final Map<Long, Set<String>> mRecognizedOdEleNames = + Map.ofEntries( + Map.entry( + 1L, + Set.of( + XmlUtils.OD_NAME_TITLE, + XmlUtils.OD_NAME_DESCRIPTION, + XmlUtils.OD_NAME_CONTAINS_ADS, + XmlUtils.OD_NAME_OBEY_APS, + XmlUtils.OD_NAME_ADS_FINGERPRINTING, + XmlUtils.OD_NAME_SECURITY_FINGERPRINTING, + XmlUtils.OD_NAME_PRIVACY_POLICY, + XmlUtils.OD_NAME_SECURITY_ENDPOINTS, + XmlUtils.OD_NAME_FIRST_PARTY_ENDPOINTS, + XmlUtils.OD_NAME_SERVICE_PROVIDER_ENDPOINTS, + XmlUtils.OD_NAME_CATEGORY, + XmlUtils.OD_NAME_EMAIL, + XmlUtils.OD_NAME_WEBSITE)), + Map.entry( + 2L, + Set.of( + XmlUtils.OD_NAME_APS_COMPLIANT, + XmlUtils.OD_NAME_PRIVACY_POLICY, + XmlUtils.OD_NAME_FIRST_PARTY_ENDPOINTS, + XmlUtils.OD_NAME_SERVICE_PROVIDER_ENDPOINTS, + XmlUtils.OD_NAME_DEVELOPER_ID, + XmlUtils.OD_NAME_APPLICATION_ID))); + private final Map<Long, Set<String>> mRequiredOdEles = + Map.ofEntries( + Map.entry( + 1L, + Set.of( + XmlUtils.OD_NAME_TITLE, + XmlUtils.OD_NAME_DESCRIPTION, + XmlUtils.OD_NAME_CONTAINS_ADS, + XmlUtils.OD_NAME_OBEY_APS, + XmlUtils.OD_NAME_ADS_FINGERPRINTING, + XmlUtils.OD_NAME_SECURITY_FINGERPRINTING, + XmlUtils.OD_NAME_PRIVACY_POLICY, + XmlUtils.OD_NAME_SECURITY_ENDPOINTS, + XmlUtils.OD_NAME_FIRST_PARTY_ENDPOINTS, + XmlUtils.OD_NAME_SERVICE_PROVIDER_ENDPOINTS, + XmlUtils.OD_NAME_CATEGORY)), + Map.entry( + 2L, + Set.of( + XmlUtils.OD_NAME_APS_COMPLIANT, + XmlUtils.OD_NAME_PRIVACY_POLICY, + XmlUtils.OD_NAME_FIRST_PARTY_ENDPOINTS, + XmlUtils.OD_NAME_SERVICE_PROVIDER_ENDPOINTS, + XmlUtils.OD_NAME_DEVELOPER_ID, + XmlUtils.OD_NAME_APPLICATION_ID))); /** Creates a {@link AppInfo} from the human-readable DOM element. */ - @Override - public AppInfo createFromHrElements(List<Element> elements) throws MalformedXmlException { - Element appInfoEle = XmlUtils.getSingleElement(elements); - if (appInfoEle == null) { - AslgenUtil.logI("No AppInfo found in hr format."); - return null; - } + public AppInfo createFromHrElement(Element appInfoEle, long version) + throws MalformedXmlException { + XmlUtils.throwIfExtraneousAttributes( + appInfoEle, XmlUtils.getMostRecentVersion(mRecognizedHrAttrs, version)); + XmlUtils.throwIfExtraneousChildrenHr( + appInfoEle, XmlUtils.getMostRecentVersion(mRecognizedHrEles, version)); - Boolean apsCompliant = - XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_APS_COMPLIANT, true); + var requiredHrAttrs = XmlUtils.getMostRecentVersion(mRequiredHrAttrs, version); + var requiredHrEles = XmlUtils.getMostRecentVersion(mRequiredHrEles, version); + + String title = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_TITLE, requiredHrAttrs); + String description = + XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_DESCRIPTION, requiredHrAttrs); + Boolean containsAds = + XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_CONTAINS_ADS, requiredHrAttrs); + Boolean obeyAps = + XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_OBEY_APS, requiredHrAttrs); + Boolean adsFingerprinting = + XmlUtils.getBoolAttr( + appInfoEle, XmlUtils.HR_ATTR_ADS_FINGERPRINTING, requiredHrAttrs); + Boolean securityFingerprinting = + XmlUtils.getBoolAttr( + appInfoEle, XmlUtils.HR_ATTR_SECURITY_FINGERPRINTING, requiredHrAttrs); String privacyPolicy = - XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_PRIVACY_POLICY, true); + XmlUtils.getStringAttr( + appInfoEle, XmlUtils.HR_ATTR_PRIVACY_POLICY, requiredHrAttrs); + List<String> securityEndpoints = + XmlUtils.getPipelineSplitAttr( + appInfoEle, XmlUtils.HR_ATTR_SECURITY_ENDPOINTS, requiredHrAttrs); + String category = + XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_CATEGORY, requiredHrAttrs); + String email = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_EMAIL, requiredHrAttrs); + String website = + XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_WEBSITE, requiredHrAttrs); + String developerId = + XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_DEVELOPER_ID, requiredHrAttrs); + String applicationId = + XmlUtils.getStringAttr( + appInfoEle, XmlUtils.HR_ATTR_APPLICATION_ID, requiredHrAttrs); + + Boolean apsCompliant = + XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_APS_COMPLIANT, requiredHrAttrs); List<String> firstPartyEndpoints = XmlUtils.getHrItemsAsStrings( - appInfoEle, XmlUtils.HR_TAG_FIRST_PARTY_ENDPOINTS, true); + appInfoEle, XmlUtils.HR_TAG_FIRST_PARTY_ENDPOINTS, requiredHrEles); List<String> serviceProviderEndpoints = XmlUtils.getHrItemsAsStrings( - appInfoEle, XmlUtils.HR_TAG_SERVICE_PROVIDER_ENDPOINTS, true); + appInfoEle, XmlUtils.HR_TAG_SERVICE_PROVIDER_ENDPOINTS, requiredHrEles); return new AppInfo( - apsCompliant, privacyPolicy, firstPartyEndpoints, serviceProviderEndpoints); + title, + description, + containsAds, + obeyAps, + adsFingerprinting, + securityFingerprinting, + privacyPolicy, + securityEndpoints, + firstPartyEndpoints, + serviceProviderEndpoints, + category, + email, + website, + apsCompliant, + developerId, + applicationId); } /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ - @Override - public AppInfo createFromOdElements(List<Element> elements) throws MalformedXmlException { - Element appInfoEle = XmlUtils.getSingleElement(elements); - if (appInfoEle == null) { - AslgenUtil.logI("No AppInfo found in od format."); - return null; - } + public AppInfo createFromOdElement(Element appInfoEle, long version) + throws MalformedXmlException { + XmlUtils.throwIfExtraneousChildrenOd( + appInfoEle, XmlUtils.getMostRecentVersion(mRecognizedOdEleNames, version)); + var requiredOdEles = XmlUtils.getMostRecentVersion(mRequiredOdEles, version); - Boolean apsCompliant = - XmlUtils.getOdBoolEle(appInfoEle, XmlUtils.OD_NAME_APS_COMPLIANT, true); + String title = XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_TITLE, requiredOdEles); + String description = + XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_DESCRIPTION, requiredOdEles); + Boolean containsAds = + XmlUtils.getOdBoolEle(appInfoEle, XmlUtils.OD_NAME_CONTAINS_ADS, requiredOdEles); + Boolean obeyAps = + XmlUtils.getOdBoolEle(appInfoEle, XmlUtils.OD_NAME_OBEY_APS, requiredOdEles); + Boolean adsFingerprinting = + XmlUtils.getOdBoolEle( + appInfoEle, XmlUtils.OD_NAME_ADS_FINGERPRINTING, requiredOdEles); + Boolean securityFingerprinting = + XmlUtils.getOdBoolEle( + appInfoEle, XmlUtils.OD_NAME_SECURITY_FINGERPRINTING, requiredOdEles); String privacyPolicy = - XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_PRIVACY_POLICY, true); + XmlUtils.getOdStringEle( + appInfoEle, XmlUtils.OD_NAME_PRIVACY_POLICY, requiredOdEles); + List<String> securityEndpoints = + XmlUtils.getOdStringArray( + appInfoEle, XmlUtils.OD_NAME_SECURITY_ENDPOINTS, requiredOdEles); + String category = + XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_CATEGORY, requiredOdEles); + String email = XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_EMAIL, requiredOdEles); + String website = + XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_WEBSITE, requiredOdEles); + String developerId = + XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_DEVELOPER_ID, requiredOdEles); + String applicationId = + XmlUtils.getOdStringEle( + appInfoEle, XmlUtils.OD_NAME_APPLICATION_ID, requiredOdEles); + + Boolean apsCompliant = + XmlUtils.getOdBoolEle(appInfoEle, XmlUtils.OD_NAME_APS_COMPLIANT, requiredOdEles); List<String> firstPartyEndpoints = - XmlUtils.getOdStringArray(appInfoEle, XmlUtils.OD_NAME_FIRST_PARTY_ENDPOINTS, true); + XmlUtils.getOdStringArray( + appInfoEle, XmlUtils.OD_NAME_FIRST_PARTY_ENDPOINTS, requiredOdEles); List<String> serviceProviderEndpoints = XmlUtils.getOdStringArray( - appInfoEle, XmlUtils.OD_NAME_SERVICE_PROVIDER_ENDPOINTS, true); + appInfoEle, XmlUtils.OD_NAME_SERVICE_PROVIDER_ENDPOINTS, requiredOdEles); return new AppInfo( - apsCompliant, privacyPolicy, firstPartyEndpoints, serviceProviderEndpoints); + title, + description, + containsAds, + obeyAps, + adsFingerprinting, + securityFingerprinting, + privacyPolicy, + securityEndpoints, + firstPartyEndpoints, + serviceProviderEndpoints, + category, + email, + website, + apsCompliant, + developerId, + applicationId); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallable.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallable.java index 0a70e7d0d74b..b6c789dc5ec7 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallable.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallable.java @@ -16,16 +16,11 @@ package com.android.asllib.marshallable; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import java.util.List; - public interface AslMarshallable { /** Creates the on-device DOM elements from the AslMarshallable Java Object. */ - List<Element> toOdDomElements(Document doc); + // List<Element> toOdDomElements(Document doc); /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - List<Element> toHrDomElements(Document doc); + // List<Element> toHrDomElements(Document doc); } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallableFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallableFactory.java index 39582900f3a0..67f106948e79 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallableFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallableFactory.java @@ -16,17 +16,11 @@ package com.android.asllib.marshallable; -import com.android.asllib.util.MalformedXmlException; - -import org.w3c.dom.Element; - -import java.util.List; - public interface AslMarshallableFactory<T extends AslMarshallable> { /** Creates an {@link AslMarshallableFactory} from human-readable DOM elements */ - T createFromHrElements(List<Element> elements) throws MalformedXmlException; + // T createFromHrElements(List<Element> elements) throws MalformedXmlException; /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ - T createFromOdElements(List<Element> elements) throws MalformedXmlException; + // T createFromOdElements(List<Element> elements) throws MalformedXmlException; } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java index c16d18b34360..501f170ad317 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java @@ -57,18 +57,16 @@ public class DataCategory implements AslMarshallable { } /** Creates on-device DOM element(s) from the {@link DataCategory}. */ - @Override - public List<Element> toOdDomElements(Document doc) { + public Element toOdDomElement(Document doc) { Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, this.getCategoryName()); for (DataType dataType : mDataTypes.values()) { - XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc)); + dataCategoryEle.appendChild(dataType.toOdDomElement(doc)); } - return XmlUtils.listOf(dataCategoryEle); + return dataCategoryEle; } /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - @Override - public List<Element> toHrDomElements(Document doc) { + public List<Element> toHrDomElement(Document doc) { throw new IllegalStateException( "Turning DataCategory or DataType into human-readable DOM elements requires" + " visibility into parent elements. The logic resides in DataLabels."); diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategoryFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategoryFactory.java index 724416285acd..fb84e508ff7f 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategoryFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategoryFactory.java @@ -23,13 +23,11 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; public class DataCategoryFactory { /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ - public DataCategory createFromOdElements(List<Element> elements) throws MalformedXmlException { - Element dataCategoryEle = XmlUtils.getSingleElement(elements); + public DataCategory createFromOdElement(Element dataCategoryEle) throws MalformedXmlException { Map<String, DataType> dataTypeMap = new LinkedHashMap<String, DataType>(); String categoryName = dataCategoryEle.getAttribute(XmlUtils.OD_ATTR_NAME); var odDataTypes = XmlUtils.asElementList(dataCategoryEle.getChildNodes()); @@ -45,9 +43,7 @@ public class DataCategoryFactory { "Unrecognized data type name %s for category %s", dataTypeName, categoryName)); } - dataTypeMap.put( - dataTypeName, - new DataTypeFactory().createFromOdElements(XmlUtils.listOf(odDataTypeEle))); + dataTypeMap.put(dataTypeName, new DataTypeFactory().createFromOdElement(odDataTypeEle)); } return new DataCategory(categoryName, dataTypeMap); diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java index 3c93c88cd060..2cf7c82fdb3e 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java @@ -22,7 +22,6 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -57,20 +56,18 @@ public class DataLabels implements AslMarshallable { } /** Gets the on-device DOM element for the {@link DataLabels}. */ - @Override - public List<Element> toOdDomElements(Document doc) { + public Element toOdDomElement(Document doc) { Element dataLabelsEle = XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS); maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED); maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED); - return XmlUtils.listOf(dataLabelsEle); + return dataLabelsEle; } /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - @Override - public List<Element> toHrDomElements(Document doc) { + public Element toHrDomElement(Document doc) { Element dataLabelsEle = doc.createElement(XmlUtils.HR_TAG_DATA_LABELS); maybeAppendHrDataUsages( doc, dataLabelsEle, mDataCollected, XmlUtils.HR_TAG_DATA_COLLECTED, false); @@ -78,7 +75,7 @@ public class DataLabels implements AslMarshallable { doc, dataLabelsEle, mDataCollected, XmlUtils.HR_TAG_DATA_COLLECTED_EPHEMERAL, true); maybeAppendHrDataUsages( doc, dataLabelsEle, mDataShared, XmlUtils.HR_TAG_DATA_SHARED, false); - return XmlUtils.listOf(dataLabelsEle); + return dataLabelsEle; } private void maybeAppendDataUsages( @@ -96,7 +93,7 @@ public class DataLabels implements AslMarshallable { DataCategory dataCategory = dataCategoriesMap.get(dataCategoryName); for (String dataTypeName : dataCategory.getDataTypes().keySet()) { DataType dataType = dataCategory.getDataTypes().get(dataTypeName); - XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc)); + dataCategoryEle.appendChild(dataType.toOdDomElement(doc)); } dataUsageEle.appendChild(dataCategoryEle); } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabelsFactory.java index c4d88761835a..b1cf3ea0d39a 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabelsFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabelsFactory.java @@ -31,9 +31,7 @@ import java.util.Map; public class DataLabelsFactory implements AslMarshallableFactory<DataLabels> { /** Creates a {@link DataLabels} from the human-readable DOM element. */ - @Override - public DataLabels createFromHrElements(List<Element> elements) throws MalformedXmlException { - Element ele = XmlUtils.getSingleElement(elements); + public DataLabels createFromHrElement(Element ele) throws MalformedXmlException { if (ele == null) { AslgenUtil.logI("Found no DataLabels in hr format."); return null; @@ -83,9 +81,7 @@ public class DataLabelsFactory implements AslMarshallableFactory<DataLabels> { } /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ - @Override - public DataLabels createFromOdElements(List<Element> elements) throws MalformedXmlException { - Element dataLabelsEle = XmlUtils.getSingleElement(elements); + public DataLabels createFromOdElement(Element dataLabelsEle) throws MalformedXmlException { if (dataLabelsEle == null) { AslgenUtil.logI("Found no DataLabels in od format."); return null; @@ -111,7 +107,7 @@ public class DataLabelsFactory implements AslMarshallableFactory<DataLabels> { for (Element dataCategoryEle : dataCategoryEles) { String dataCategoryName = dataCategoryEle.getAttribute(XmlUtils.OD_ATTR_NAME); DataCategory dataCategory = - new DataCategoryFactory().createFromOdElements(List.of(dataCategoryEle)); + new DataCategoryFactory().createFromOdElement(dataCategoryEle); dataCategoryMap.put(dataCategoryName, dataCategory); } return dataCategoryMap; diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java index 284a4b804435..83bb61144efa 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java @@ -172,8 +172,8 @@ public class DataType implements AslMarshallable { return mEphemeral; } - @Override - public List<Element> toOdDomElements(Document doc) { + /** Gets the on-device dom element */ + public Element toOdDomElement(Document doc) { Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, this.getDataTypeName()); if (!this.getPurposes().isEmpty()) { dataTypeEle.appendChild( @@ -197,11 +197,10 @@ public class DataType implements AslMarshallable { this.getIsSharingOptional(), XmlUtils.OD_NAME_IS_SHARING_OPTIONAL); maybeAddBoolToOdElement(doc, dataTypeEle, this.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL); - return XmlUtils.listOf(dataTypeEle); + return dataTypeEle; } /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - @Override public List<Element> toHrDomElements(Document doc) { throw new IllegalStateException( "Turning DataCategory or DataType into human-readable DOM elements requires" diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataTypeFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataTypeFactory.java index a5559d801349..96a58fa34707 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataTypeFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataTypeFactory.java @@ -64,8 +64,7 @@ public class DataTypeFactory { } /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ - public DataType createFromOdElements(List<Element> elements) throws MalformedXmlException { - Element odDataTypeEle = XmlUtils.getSingleElement(elements); + public DataType createFromOdElement(Element odDataTypeEle) throws MalformedXmlException { String dataTypeName = odDataTypeEle.getAttribute(XmlUtils.OD_ATTR_NAME); List<Integer> purposeInts = XmlUtils.getOdIntArray(odDataTypeEle, XmlUtils.OD_NAME_PURPOSES, true); diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java new file mode 100644 index 000000000000..96a64dc29526 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java @@ -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.asllib.marshallable; + +import com.android.asllib.util.XmlUtils; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** DeveloperInfo representation */ +public class DeveloperInfo implements AslMarshallable { + public enum DeveloperRelationship { + OEM(0), + ODM(1), + SOC(2), + OTA(3), + CARRIER(4), + AOSP(5), + OTHER(6); + + private final int mValue; + + DeveloperRelationship(int value) { + this.mValue = value; + } + + /** Get the int value associated with the DeveloperRelationship. */ + public int getValue() { + return mValue; + } + + /** Get the DeveloperRelationship associated with the int value. */ + public static DeveloperInfo.DeveloperRelationship forValue(int value) { + for (DeveloperInfo.DeveloperRelationship e : values()) { + if (e.getValue() == value) { + return e; + } + } + throw new IllegalArgumentException("No DeveloperRelationship enum for value: " + value); + } + + /** Get the DeveloperRelationship associated with the human-readable String. */ + public static DeveloperInfo.DeveloperRelationship forString(String s) { + for (DeveloperInfo.DeveloperRelationship e : values()) { + if (e.toString().equals(s)) { + return e; + } + } + throw new IllegalArgumentException("No DeveloperRelationship enum for str: " + s); + } + + /** Human-readable String representation of DeveloperRelationship. */ + public String toString() { + return this.name().toLowerCase(); + } + } + + private final String mName; + private final String mEmail; + private final String mAddress; + private final String mCountryRegion; + private final DeveloperRelationship mDeveloperRelationship; + private final String mWebsite; + private final String mAppDeveloperRegistryId; + + public DeveloperInfo( + String name, + String email, + String address, + String countryRegion, + DeveloperRelationship developerRelationship, + String website, + String appDeveloperRegistryId) { + this.mName = name; + this.mEmail = email; + this.mAddress = address; + this.mCountryRegion = countryRegion; + this.mDeveloperRelationship = developerRelationship; + this.mWebsite = website; + this.mAppDeveloperRegistryId = appDeveloperRegistryId; + } + + /** Creates an on-device DOM element from the {@link SafetyLabels}. */ + public Element toOdDomElement(Document doc) { + Element developerInfoEle = + XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DEVELOPER_INFO); + if (mName != null) { + developerInfoEle.appendChild( + XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_NAME, mName)); + } + if (mEmail != null) { + developerInfoEle.appendChild( + XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_EMAIL, mEmail)); + } + if (mAddress != null) { + developerInfoEle.appendChild( + XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_ADDRESS, mAddress)); + } + if (mCountryRegion != null) { + developerInfoEle.appendChild( + XmlUtils.createOdStringEle( + doc, XmlUtils.OD_NAME_COUNTRY_REGION, mCountryRegion)); + } + if (mDeveloperRelationship != null) { + developerInfoEle.appendChild( + XmlUtils.createOdLongEle( + doc, + XmlUtils.OD_NAME_DEVELOPER_RELATIONSHIP, + mDeveloperRelationship.getValue())); + } + if (mWebsite != null) { + developerInfoEle.appendChild( + XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_WEBSITE, mWebsite)); + } + if (mAppDeveloperRegistryId != null) { + developerInfoEle.appendChild( + XmlUtils.createOdStringEle( + doc, + XmlUtils.OD_NAME_APP_DEVELOPER_REGISTRY_ID, + mAppDeveloperRegistryId)); + } + return developerInfoEle; + } + + /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ + public Element toHrDomElement(Document doc) { + Element developerInfoEle = doc.createElement(XmlUtils.HR_TAG_DEVELOPER_INFO); + if (mName != null) { + developerInfoEle.setAttribute(XmlUtils.HR_ATTR_NAME, mName); + } + if (mEmail != null) { + developerInfoEle.setAttribute(XmlUtils.HR_ATTR_EMAIL, mEmail); + } + if (mAddress != null) { + developerInfoEle.setAttribute(XmlUtils.HR_ATTR_ADDRESS, mAddress); + } + if (mCountryRegion != null) { + developerInfoEle.setAttribute(XmlUtils.HR_ATTR_COUNTRY_REGION, mCountryRegion); + } + if (mDeveloperRelationship != null) { + developerInfoEle.setAttribute( + XmlUtils.HR_ATTR_DEVELOPER_RELATIONSHIP, mDeveloperRelationship.toString()); + } + if (mWebsite != null) { + developerInfoEle.setAttribute(XmlUtils.HR_ATTR_WEBSITE, mWebsite); + } + if (mAppDeveloperRegistryId != null) { + developerInfoEle.setAttribute( + XmlUtils.HR_ATTR_APP_DEVELOPER_REGISTRY_ID, mAppDeveloperRegistryId); + } + return developerInfoEle; + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java new file mode 100644 index 000000000000..e82a53a53b25 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java @@ -0,0 +1,89 @@ +/* + * 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.asllib.marshallable; + +import com.android.asllib.util.AslgenUtil; +import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; + +import org.w3c.dom.Element; + +public class DeveloperInfoFactory implements AslMarshallableFactory<DeveloperInfo> { + /** Creates a {@link DeveloperInfo} from the human-readable DOM element. */ + public DeveloperInfo createFromHrElement(Element developerInfoEle) + throws MalformedXmlException { + if (developerInfoEle == null) { + AslgenUtil.logI("No DeveloperInfo found in hr format."); + return null; + } + String name = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_NAME, true); + String email = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_EMAIL, true); + String address = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_ADDRESS, true); + String countryRegion = + XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_COUNTRY_REGION, true); + DeveloperInfo.DeveloperRelationship developerRelationship = + DeveloperInfo.DeveloperRelationship.forString( + XmlUtils.getStringAttr( + developerInfoEle, XmlUtils.HR_ATTR_DEVELOPER_RELATIONSHIP, true)); + String website = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_WEBSITE, false); + String appDeveloperRegistryId = + XmlUtils.getStringAttr( + developerInfoEle, XmlUtils.HR_ATTR_APP_DEVELOPER_REGISTRY_ID, false); + return new DeveloperInfo( + name, + email, + address, + countryRegion, + developerRelationship, + website, + appDeveloperRegistryId); + } + + /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ + public DeveloperInfo createFromOdElement(Element developerInfoEle) + throws MalformedXmlException { + if (developerInfoEle == null) { + AslgenUtil.logI("No DeveloperInfo found in od format."); + return null; + } + String name = XmlUtils.getOdStringEle(developerInfoEle, XmlUtils.OD_NAME_NAME, true); + String email = XmlUtils.getOdStringEle(developerInfoEle, XmlUtils.OD_NAME_EMAIL, true); + String address = XmlUtils.getOdStringEle(developerInfoEle, XmlUtils.OD_NAME_ADDRESS, true); + String countryRegion = + XmlUtils.getOdStringEle(developerInfoEle, XmlUtils.OD_NAME_COUNTRY_REGION, true); + DeveloperInfo.DeveloperRelationship developerRelationship = + DeveloperInfo.DeveloperRelationship.forValue( + (int) + (long) + XmlUtils.getOdLongEle( + developerInfoEle, + XmlUtils.OD_NAME_DEVELOPER_RELATIONSHIP, + true)); + String website = XmlUtils.getOdStringEle(developerInfoEle, XmlUtils.OD_NAME_WEBSITE, false); + String appDeveloperRegistryId = + XmlUtils.getOdStringEle( + developerInfoEle, XmlUtils.OD_NAME_APP_DEVELOPER_REGISTRY_ID, false); + return new DeveloperInfo( + name, + email, + address, + countryRegion, + developerRelationship, + website, + appDeveloperRegistryId); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java index 2a4e130981af..1a83c02de3e4 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java @@ -21,40 +21,50 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; -import java.util.List; - /** Safety Label representation containing zero or more {@link DataCategory} for data shared */ public class SafetyLabels implements AslMarshallable { private final DataLabels mDataLabels; + private final SecurityLabels mSecurityLabels; + private final ThirdPartyVerification mThirdPartyVerification; - public SafetyLabels(DataLabels dataLabels) { + public SafetyLabels( + DataLabels dataLabels, + SecurityLabels securityLabels, + ThirdPartyVerification thirdPartyVerification) { this.mDataLabels = dataLabels; - } - - /** Returns the data label for the safety label */ - public DataLabels getDataLabel() { - return mDataLabels; + this.mSecurityLabels = securityLabels; + this.mThirdPartyVerification = thirdPartyVerification; } /** Creates an on-device DOM element from the {@link SafetyLabels}. */ - @Override - public List<Element> toOdDomElements(Document doc) { + public Element toOdDomElement(Document doc) { Element safetyLabelsEle = XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS); if (mDataLabels != null) { - XmlUtils.appendChildren(safetyLabelsEle, mDataLabels.toOdDomElements(doc)); + safetyLabelsEle.appendChild(mDataLabels.toOdDomElement(doc)); + } + if (mSecurityLabels != null) { + safetyLabelsEle.appendChild(mSecurityLabels.toOdDomElement(doc)); } - return XmlUtils.listOf(safetyLabelsEle); + if (mThirdPartyVerification != null) { + safetyLabelsEle.appendChild(mThirdPartyVerification.toOdDomElement(doc)); + } + return safetyLabelsEle; } /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - @Override - public List<Element> toHrDomElements(Document doc) { + public Element toHrDomElement(Document doc) { Element safetyLabelsEle = doc.createElement(XmlUtils.HR_TAG_SAFETY_LABELS); if (mDataLabels != null) { - XmlUtils.appendChildren(safetyLabelsEle, mDataLabels.toHrDomElements(doc)); + safetyLabelsEle.appendChild(mDataLabels.toHrDomElement(doc)); + } + if (mSecurityLabels != null) { + safetyLabelsEle.appendChild(mSecurityLabels.toHrDomElement(doc)); + } + if (mThirdPartyVerification != null) { + safetyLabelsEle.appendChild(mThirdPartyVerification.toHrDomElement(doc)); } - return XmlUtils.listOf(safetyLabelsEle); + return safetyLabelsEle; } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java index 2738337b7080..35804d9351b4 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java @@ -16,54 +16,109 @@ package com.android.asllib.marshallable; -import com.android.asllib.util.AslgenUtil; import com.android.asllib.util.MalformedXmlException; import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; -import java.util.List; +import java.util.Map; +import java.util.Set; public class SafetyLabelsFactory implements AslMarshallableFactory<SafetyLabels> { + private final Map<Long, Set<String>> mRecognizedHrAttrs = + Map.ofEntries(Map.entry(1L, Set.of())); + private final Map<Long, Set<String>> mRequiredHrAttrs = Map.ofEntries(Map.entry(1L, Set.of())); + private final Map<Long, Set<String>> mRecognizedHrEles = + Map.ofEntries( + Map.entry( + 1L, + Set.of( + XmlUtils.HR_TAG_DATA_LABELS, + XmlUtils.HR_TAG_SECURITY_LABELS, + XmlUtils.HR_TAG_THIRD_PARTY_VERIFICATION)), + Map.entry(2L, Set.of(XmlUtils.HR_TAG_DATA_LABELS))); + private final Map<Long, Set<String>> mRequiredHrEles = Map.ofEntries(Map.entry(1L, Set.of())); + private final Map<Long, Set<String>> mRecognizedOdEleNames = + Map.ofEntries( + Map.entry( + 1L, + Set.of( + XmlUtils.OD_NAME_DATA_LABELS, + XmlUtils.OD_NAME_SECURITY_LABELS, + XmlUtils.OD_NAME_THIRD_PARTY_VERIFICATION)), + Map.entry(2L, Set.of(XmlUtils.OD_NAME_DATA_LABELS))); + private final Map<Long, Set<String>> mRequiredOdEles = Map.ofEntries(Map.entry(1L, Set.of())); /** Creates a {@link SafetyLabels} from the human-readable DOM element. */ - @Override - public SafetyLabels createFromHrElements(List<Element> elements) throws MalformedXmlException { - Element safetyLabelsEle = XmlUtils.getSingleElement(elements); + public SafetyLabels createFromHrElement(Element safetyLabelsEle, long version) + throws MalformedXmlException { if (safetyLabelsEle == null) { - AslgenUtil.logI("No SafetyLabels found in hr format."); return null; } + XmlUtils.throwIfExtraneousAttributes( + safetyLabelsEle, XmlUtils.getMostRecentVersion(mRecognizedHrAttrs, version)); + XmlUtils.throwIfExtraneousChildrenHr( + safetyLabelsEle, XmlUtils.getMostRecentVersion(mRecognizedHrEles, version)); + + var requiredHrEles = XmlUtils.getMostRecentVersion(mRequiredHrEles, version); + DataLabels dataLabels = new DataLabelsFactory() - .createFromHrElements( - XmlUtils.listOf( - XmlUtils.getSingleChildElement( - safetyLabelsEle, - XmlUtils.HR_TAG_DATA_LABELS, - false))); - return new SafetyLabels(dataLabels); + .createFromHrElement( + XmlUtils.getSingleChildElement( + safetyLabelsEle, + XmlUtils.HR_TAG_DATA_LABELS, + requiredHrEles)); + SecurityLabels securityLabels = + new SecurityLabelsFactory() + .createFromHrElement( + XmlUtils.getSingleChildElement( + safetyLabelsEle, + XmlUtils.HR_TAG_SECURITY_LABELS, + requiredHrEles)); + ThirdPartyVerification thirdPartyVerification = + new ThirdPartyVerificationFactory() + .createFromHrElement( + XmlUtils.getSingleChildElement( + safetyLabelsEle, + XmlUtils.HR_TAG_THIRD_PARTY_VERIFICATION, + requiredHrEles)); + return new SafetyLabels(dataLabels, securityLabels, thirdPartyVerification); } /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ - @Override - public SafetyLabels createFromOdElements(List<Element> elements) throws MalformedXmlException { - Element safetyLabelsEle = XmlUtils.getSingleElement(elements); + public SafetyLabels createFromOdElement(Element safetyLabelsEle, long version) + throws MalformedXmlException { if (safetyLabelsEle == null) { - AslgenUtil.logI("No SafetyLabels found in od format."); return null; } + XmlUtils.throwIfExtraneousChildrenOd( + safetyLabelsEle, XmlUtils.getMostRecentVersion(mRecognizedOdEleNames, version)); + var requiredOdEles = XmlUtils.getMostRecentVersion(mRequiredOdEles, version); + DataLabels dataLabels = new DataLabelsFactory() - .createFromOdElements( - XmlUtils.listOf( - XmlUtils.getOdPbundleWithName( - safetyLabelsEle, - XmlUtils.OD_NAME_DATA_LABELS, - false))); - - return new SafetyLabels(dataLabels); + .createFromOdElement( + XmlUtils.getOdPbundleWithName( + safetyLabelsEle, + XmlUtils.OD_NAME_DATA_LABELS, + requiredOdEles)); + SecurityLabels securityLabels = + new SecurityLabelsFactory() + .createFromOdElement( + XmlUtils.getOdPbundleWithName( + safetyLabelsEle, + XmlUtils.OD_NAME_SECURITY_LABELS, + requiredOdEles)); + ThirdPartyVerification thirdPartyVerification = + new ThirdPartyVerificationFactory() + .createFromOdElement( + XmlUtils.getOdPbundleWithName( + safetyLabelsEle, + XmlUtils.OD_NAME_THIRD_PARTY_VERIFICATION, + requiredOdEles)); + return new SafetyLabels(dataLabels, securityLabels, thirdPartyVerification); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabels.java index 48643ba0e3ab..ccb84451c96f 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabels.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabels.java @@ -21,8 +21,6 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; -import java.util.List; - /** Security Labels representation */ public class SecurityLabels implements AslMarshallable { @@ -35,8 +33,7 @@ public class SecurityLabels implements AslMarshallable { } /** Creates an on-device DOM element from the {@link SecurityLabels}. */ - @Override - public List<Element> toOdDomElements(Document doc) { + public Element toOdDomElement(Document doc) { Element ele = XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SECURITY_LABELS); if (mIsDataDeletable != null) { ele.appendChild( @@ -48,12 +45,11 @@ public class SecurityLabels implements AslMarshallable { XmlUtils.createOdBooleanEle( doc, XmlUtils.OD_NAME_IS_DATA_ENCRYPTED, mIsDataEncrypted)); } - return XmlUtils.listOf(ele); + return ele; } /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - @Override - public List<Element> toHrDomElements(Document doc) { + public Element toHrDomElement(Document doc) { Element ele = doc.createElement(XmlUtils.HR_TAG_SECURITY_LABELS); if (mIsDataDeletable != null) { ele.setAttribute(XmlUtils.HR_ATTR_IS_DATA_DELETABLE, String.valueOf(mIsDataDeletable)); @@ -61,6 +57,6 @@ public class SecurityLabels implements AslMarshallable { if (mIsDataEncrypted != null) { ele.setAttribute(XmlUtils.HR_ATTR_IS_DATA_ENCRYPTED, String.valueOf(mIsDataEncrypted)); } - return XmlUtils.listOf(ele); + return ele; } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabelsFactory.java index 525a80388261..da98a4291b4a 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabelsFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabelsFactory.java @@ -22,15 +22,10 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; -import java.util.List; - public class SecurityLabelsFactory implements AslMarshallableFactory<SecurityLabels> { /** Creates a {@link SecurityLabels} from the human-readable DOM element. */ - @Override - public SecurityLabels createFromHrElements(List<Element> elements) - throws MalformedXmlException { - Element ele = XmlUtils.getSingleElement(elements); + public SecurityLabels createFromHrElement(Element ele) throws MalformedXmlException { if (ele == null) { AslgenUtil.logI("No SecurityLabels found in hr format."); return null; @@ -43,10 +38,7 @@ public class SecurityLabelsFactory implements AslMarshallableFactory<SecurityLab } /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ - @Override - public SecurityLabels createFromOdElements(List<Element> elements) - throws MalformedXmlException { - Element ele = XmlUtils.getSingleElement(elements); + public SecurityLabels createFromOdElement(Element ele) throws MalformedXmlException { if (ele == null) { AslgenUtil.logI("No SecurityLabels found in od format."); return null; diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java index 242e7be66d76..10d6e1aaac7d 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java @@ -21,34 +21,43 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; -import java.util.List; - /** Safety Label representation containing zero or more {@link DataCategory} for data shared */ public class SystemAppSafetyLabel implements AslMarshallable { + private final String mUrl; private final Boolean mDeclaration; - public SystemAppSafetyLabel(Boolean d) { + public SystemAppSafetyLabel(String url, Boolean d) { this.mDeclaration = d; + this.mUrl = null; } /** Creates an on-device DOM element from the {@link SystemAppSafetyLabel}. */ - @Override - public List<Element> toOdDomElements(Document doc) { + public Element toOdDomElement(Document doc) { Element systemAppSafetyLabelEle = XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SYSTEM_APP_SAFETY_LABEL); - systemAppSafetyLabelEle.appendChild( - XmlUtils.createOdBooleanEle(doc, XmlUtils.OD_NAME_DECLARATION, mDeclaration)); - return XmlUtils.listOf(systemAppSafetyLabelEle); + if (mUrl != null) { + systemAppSafetyLabelEle.appendChild( + XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_URL, mUrl)); + } + if (mDeclaration != null) { + systemAppSafetyLabelEle.appendChild( + XmlUtils.createOdBooleanEle(doc, XmlUtils.OD_NAME_DECLARATION, mDeclaration)); + } + return systemAppSafetyLabelEle; } /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - @Override - public List<Element> toHrDomElements(Document doc) { + public Element toHrDomElement(Document doc) { Element systemAppSafetyLabelEle = doc.createElement(XmlUtils.HR_TAG_SYSTEM_APP_SAFETY_LABEL); - XmlUtils.maybeSetHrBoolAttr( - systemAppSafetyLabelEle, XmlUtils.HR_ATTR_DECLARATION, mDeclaration); - return XmlUtils.listOf(systemAppSafetyLabelEle); + if (mUrl != null) { + systemAppSafetyLabelEle.setAttribute(XmlUtils.HR_ATTR_URL, mUrl); + } + if (mDeclaration != null) { + systemAppSafetyLabelEle.setAttribute( + XmlUtils.HR_ATTR_DECLARATION, String.valueOf(mDeclaration)); + } + return systemAppSafetyLabelEle; } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java index 7f4aa7a3b690..971ae9296eb8 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java @@ -16,42 +16,77 @@ package com.android.asllib.marshallable; -import com.android.asllib.util.AslgenUtil; import com.android.asllib.util.MalformedXmlException; import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; -import java.util.List; +import java.util.Map; +import java.util.Set; public class SystemAppSafetyLabelFactory implements AslMarshallableFactory<SystemAppSafetyLabel> { + private final Map<Long, Set<String>> mRecognizedHrAttrs = + Map.ofEntries( + Map.entry(1L, Set.of(XmlUtils.HR_ATTR_URL)), + Map.entry(2L, Set.of(XmlUtils.HR_ATTR_DECLARATION))); + private final Map<Long, Set<String>> mRequiredHrAttrs = + Map.ofEntries( + Map.entry(1L, Set.of(XmlUtils.HR_ATTR_URL)), + Map.entry(2L, Set.of(XmlUtils.HR_ATTR_DECLARATION))); + private final Map<Long, Set<String>> mRecognizedHrEles = Map.ofEntries(Map.entry(1L, Set.of())); + private final Map<Long, Set<String>> mRecognizedOdEleNames = + Map.ofEntries( + Map.entry(1L, Set.of(XmlUtils.OD_NAME_URL)), + Map.entry(2L, Set.of(XmlUtils.OD_NAME_DECLARATION))); + private final Map<Long, Set<String>> mRequiredOdEleNames = + Map.ofEntries( + Map.entry(1L, Set.of(XmlUtils.OD_NAME_URL)), + Map.entry(2L, Set.of(XmlUtils.OD_NAME_DECLARATION))); /** Creates a {@link SystemAppSafetyLabel} from the human-readable DOM element. */ - @Override - public SystemAppSafetyLabel createFromHrElements(List<Element> elements) + public SystemAppSafetyLabel createFromHrElement(Element systemAppSafetyLabelEle, long version) throws MalformedXmlException { - Element systemAppSafetyLabelEle = XmlUtils.getSingleElement(elements); if (systemAppSafetyLabelEle == null) { - AslgenUtil.logI("No SystemAppSafetyLabel found in hr format."); return null; } + XmlUtils.throwIfExtraneousAttributes( + systemAppSafetyLabelEle, + XmlUtils.getMostRecentVersion(mRecognizedHrAttrs, version)); + XmlUtils.throwIfExtraneousChildrenHr( + systemAppSafetyLabelEle, XmlUtils.getMostRecentVersion(mRecognizedHrEles, version)); + String url = + XmlUtils.getStringAttr( + systemAppSafetyLabelEle, + XmlUtils.HR_ATTR_URL, + XmlUtils.getMostRecentVersion(mRequiredHrAttrs, version)); Boolean declaration = - XmlUtils.getBoolAttr(systemAppSafetyLabelEle, XmlUtils.HR_ATTR_DECLARATION, true); - return new SystemAppSafetyLabel(declaration); + XmlUtils.getBoolAttr( + systemAppSafetyLabelEle, + XmlUtils.HR_ATTR_DECLARATION, + XmlUtils.getMostRecentVersion(mRequiredHrAttrs, version)); + return new SystemAppSafetyLabel(url, declaration); } /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ - @Override - public SystemAppSafetyLabel createFromOdElements(List<Element> elements) + public SystemAppSafetyLabel createFromOdElement(Element systemAppSafetyLabelEle, long version) throws MalformedXmlException { - Element systemAppSafetyLabelEle = XmlUtils.getSingleElement(elements); if (systemAppSafetyLabelEle == null) { - AslgenUtil.logI("No SystemAppSafetyLabel found in od format."); return null; } + XmlUtils.throwIfExtraneousChildrenOd( + systemAppSafetyLabelEle, + XmlUtils.getMostRecentVersion(mRecognizedOdEleNames, version)); + String url = + XmlUtils.getOdStringEle( + systemAppSafetyLabelEle, + XmlUtils.OD_NAME_URL, + XmlUtils.getMostRecentVersion(mRequiredOdEleNames, version)); Boolean declaration = - XmlUtils.getOdBoolEle(systemAppSafetyLabelEle, XmlUtils.OD_NAME_DECLARATION, true); - return new SystemAppSafetyLabel(declaration); + XmlUtils.getOdBoolEle( + systemAppSafetyLabelEle, + XmlUtils.OD_NAME_DECLARATION, + XmlUtils.getMostRecentVersion(mRequiredOdEleNames, version)); + return new SystemAppSafetyLabel(url, declaration); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerification.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerification.java index d74f3f062513..151cdb1e2f51 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerification.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerification.java @@ -21,8 +21,6 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; -import java.util.List; - /** ThirdPartyVerification representation. */ public class ThirdPartyVerification implements AslMarshallable { @@ -33,19 +31,17 @@ public class ThirdPartyVerification implements AslMarshallable { } /** Creates an on-device DOM element from the {@link ThirdPartyVerification}. */ - @Override - public List<Element> toOdDomElements(Document doc) { + public Element toOdDomElement(Document doc) { Element ele = XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_THIRD_PARTY_VERIFICATION); ele.appendChild(XmlUtils.createOdStringEle(doc, XmlUtils.OD_NAME_URL, mUrl)); - return XmlUtils.listOf(ele); + return ele; } /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - @Override - public List<Element> toHrDomElements(Document doc) { + public Element toHrDomElement(Document doc) { Element ele = doc.createElement(XmlUtils.HR_TAG_THIRD_PARTY_VERIFICATION); ele.setAttribute(XmlUtils.HR_ATTR_URL, mUrl); - return XmlUtils.listOf(ele); + return ele; } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerificationFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerificationFactory.java index 197e7aa77743..f229ad6c861f 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerificationFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerificationFactory.java @@ -22,16 +22,11 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; -import java.util.List; - public class ThirdPartyVerificationFactory implements AslMarshallableFactory<ThirdPartyVerification> { /** Creates a {@link ThirdPartyVerification} from the human-readable DOM element. */ - @Override - public ThirdPartyVerification createFromHrElements(List<Element> elements) - throws MalformedXmlException { - Element ele = XmlUtils.getSingleElement(elements); + public ThirdPartyVerification createFromHrElement(Element ele) throws MalformedXmlException { if (ele == null) { AslgenUtil.logI("No ThirdPartyVerification found in hr format."); return null; @@ -42,10 +37,7 @@ public class ThirdPartyVerificationFactory } /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ - @Override - public ThirdPartyVerification createFromOdElements(List<Element> elements) - throws MalformedXmlException { - Element ele = XmlUtils.getSingleElement(elements); + public ThirdPartyVerification createFromOdElement(Element ele) throws MalformedXmlException { if (ele == null) { AslgenUtil.logI("No ThirdPartyVerification found in od format."); return null; diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java index 9f789f0b9c1c..f24e6bf58efb 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java @@ -21,39 +21,39 @@ import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; -import java.util.List; - /** TransparencyInfo representation containing {@link AppInfo} */ public class TransparencyInfo implements AslMarshallable { + + private final DeveloperInfo mDeveloperInfo; private final AppInfo mAppInfo; - public TransparencyInfo(AppInfo appInfo) { + public TransparencyInfo(DeveloperInfo developerInfo, AppInfo appInfo) { + this.mDeveloperInfo = developerInfo; this.mAppInfo = appInfo; } - /** Gets the {@link AppInfo} of the {@link TransparencyInfo}. */ - public AppInfo getAppInfo() { - return mAppInfo; - } - /** Creates an on-device DOM element from the {@link TransparencyInfo}. */ - @Override - public List<Element> toOdDomElements(Document doc) { + public Element toOdDomElement(Document doc) { Element transparencyInfoEle = XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_TRANSPARENCY_INFO); + if (mDeveloperInfo != null) { + transparencyInfoEle.appendChild(mDeveloperInfo.toOdDomElement(doc)); + } if (mAppInfo != null) { - XmlUtils.appendChildren(transparencyInfoEle, mAppInfo.toOdDomElements(doc)); + transparencyInfoEle.appendChild(mAppInfo.toOdDomElement(doc)); } - return XmlUtils.listOf(transparencyInfoEle); + return transparencyInfoEle; } /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ - @Override - public List<Element> toHrDomElements(Document doc) { + public Element toHrDomElement(Document doc) { Element transparencyInfoEle = doc.createElement(XmlUtils.HR_TAG_TRANSPARENCY_INFO); + if (mDeveloperInfo != null) { + transparencyInfoEle.appendChild(mDeveloperInfo.toHrDomElement(doc)); + } if (mAppInfo != null) { - XmlUtils.appendChildren(transparencyInfoEle, mAppInfo.toHrDomElements(doc)); + transparencyInfoEle.appendChild(mAppInfo.toHrDomElement(doc)); } - return XmlUtils.listOf(transparencyInfoEle); + return transparencyInfoEle; } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java index 40f2872e4380..9e98941e0d6d 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java @@ -16,47 +16,84 @@ package com.android.asllib.marshallable; -import com.android.asllib.util.AslgenUtil; import com.android.asllib.util.MalformedXmlException; import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; -import java.util.List; +import java.util.Map; +import java.util.Set; public class TransparencyInfoFactory implements AslMarshallableFactory<TransparencyInfo> { + private final Map<Long, Set<String>> mRecognizedHrAttrs = + Map.ofEntries(Map.entry(1L, Set.of())); + private final Map<Long, Set<String>> mRequiredHrAttrs = Map.ofEntries(Map.entry(1L, Set.of())); + private final Map<Long, Set<String>> mRecognizedHrEles = + Map.ofEntries( + Map.entry(1L, Set.of(XmlUtils.HR_TAG_DEVELOPER_INFO, XmlUtils.HR_TAG_APP_INFO)), + Map.entry(2L, Set.of(XmlUtils.HR_TAG_APP_INFO))); + private final Map<Long, Set<String>> mRequiredHrEles = + Map.ofEntries(Map.entry(1L, Set.of()), Map.entry(2L, Set.of(XmlUtils.HR_TAG_APP_INFO))); + private final Map<Long, Set<String>> mRecognizedOdEleNames = + Map.ofEntries( + Map.entry( + 1L, Set.of(XmlUtils.OD_NAME_DEVELOPER_INFO, XmlUtils.OD_NAME_APP_INFO)), + Map.entry(2L, Set.of(XmlUtils.OD_NAME_APP_INFO))); + private final Map<Long, Set<String>> mRequiredOdEles = + Map.ofEntries( + Map.entry(1L, Set.of()), Map.entry(2L, Set.of(XmlUtils.OD_NAME_APP_INFO))); /** Creates a {@link TransparencyInfo} from the human-readable DOM element. */ - @Override - public TransparencyInfo createFromHrElements(List<Element> elements) + public TransparencyInfo createFromHrElement(Element transparencyInfoEle, long version) throws MalformedXmlException { - Element transparencyInfoEle = XmlUtils.getSingleElement(elements); if (transparencyInfoEle == null) { - AslgenUtil.logI("No TransparencyInfo found in hr format."); return null; } + XmlUtils.throwIfExtraneousAttributes( + transparencyInfoEle, XmlUtils.getMostRecentVersion(mRecognizedHrAttrs, version)); + XmlUtils.throwIfExtraneousChildrenHr( + transparencyInfoEle, XmlUtils.getMostRecentVersion(mRecognizedHrEles, version)); + Element developerInfoEle = + XmlUtils.getSingleChildElement( + transparencyInfoEle, + XmlUtils.HR_TAG_DEVELOPER_INFO, + XmlUtils.getMostRecentVersion(mRequiredHrEles, version)); + DeveloperInfo developerInfo = + new DeveloperInfoFactory().createFromHrElement(developerInfoEle); Element appInfoEle = - XmlUtils.getSingleChildElement(transparencyInfoEle, XmlUtils.HR_TAG_APP_INFO, true); - AppInfo appInfo = new AppInfoFactory().createFromHrElements(XmlUtils.listOf(appInfoEle)); + XmlUtils.getSingleChildElement( + transparencyInfoEle, + XmlUtils.HR_TAG_APP_INFO, + XmlUtils.getMostRecentVersion(mRequiredHrEles, version)); + AppInfo appInfo = new AppInfoFactory().createFromHrElement(appInfoEle, version); - return new TransparencyInfo(appInfo); + return new TransparencyInfo(developerInfo, appInfo); } /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ - @Override - public TransparencyInfo createFromOdElements(List<Element> elements) + public TransparencyInfo createFromOdElement(Element transparencyInfoEle, long version) throws MalformedXmlException { - Element transparencyInfoEle = XmlUtils.getSingleElement(elements); if (transparencyInfoEle == null) { - AslgenUtil.logI("No TransparencyInfo found in od format."); return null; } + XmlUtils.throwIfExtraneousChildrenOd( + transparencyInfoEle, XmlUtils.getMostRecentVersion(mRecognizedOdEleNames, version)); + Element developerInfoEle = + XmlUtils.getOdPbundleWithName( + transparencyInfoEle, + XmlUtils.OD_NAME_DEVELOPER_INFO, + XmlUtils.getMostRecentVersion(mRequiredOdEles, version)); + DeveloperInfo developerInfo = + new DeveloperInfoFactory().createFromOdElement(developerInfoEle); Element appInfoEle = - XmlUtils.getOdPbundleWithName(transparencyInfoEle, XmlUtils.OD_NAME_APP_INFO, true); - AppInfo appInfo = new AppInfoFactory().createFromOdElements(XmlUtils.listOf(appInfoEle)); + XmlUtils.getOdPbundleWithName( + transparencyInfoEle, + XmlUtils.OD_NAME_APP_INFO, + XmlUtils.getMostRecentVersion(mRequiredOdEles, version)); + AppInfo appInfo = new AppInfoFactory().createFromOdElement(appInfoEle, version); - return new TransparencyInfo(appInfo); + return new TransparencyInfo(developerInfo, appInfo); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java index 2c1517bdf8ab..52c4390036f3 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java @@ -25,6 +25,8 @@ import org.w3c.dom.NodeList; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; public class XmlUtils { @@ -70,6 +72,8 @@ public class XmlUtils { public static final String HR_ATTR_ADS_FINGERPRINTING = "adsFingerprinting"; public static final String HR_ATTR_SECURITY_FINGERPRINTING = "securityFingerprinting"; public static final String HR_ATTR_PRIVACY_POLICY = "privacyPolicy"; + public static final String HR_ATTR_DEVELOPER_ID = "developerId"; + public static final String HR_ATTR_APPLICATION_ID = "applicationId"; public static final String HR_ATTR_SECURITY_ENDPOINTS = "securityEndpoints"; public static final String HR_TAG_FIRST_PARTY_ENDPOINTS = "first-party-endpoints"; public static final String HR_TAG_SERVICE_PROVIDER_ENDPOINTS = "service-provider-endpoints"; @@ -102,10 +106,12 @@ public class XmlUtils { public static final String OD_NAME_CONTAINS_ADS = "contains_ads"; public static final String OD_NAME_OBEY_APS = "obey_aps"; public static final String OD_NAME_APS_COMPLIANT = "aps_compliant"; + public static final String OD_NAME_DEVELOPER_ID = "developer_id"; + public static final String OD_NAME_APPLICATION_ID = "application_id"; public static final String OD_NAME_ADS_FINGERPRINTING = "ads_fingerprinting"; public static final String OD_NAME_SECURITY_FINGERPRINTING = "security_fingerprinting"; public static final String OD_NAME_PRIVACY_POLICY = "privacy_policy"; - public static final String OD_NAME_SECURITY_ENDPOINT = "security_endpoints"; + public static final String OD_NAME_SECURITY_ENDPOINTS = "security_endpoints"; public static final String OD_NAME_FIRST_PARTY_ENDPOINTS = "first_party_endpoints"; public static final String OD_NAME_SERVICE_PROVIDER_ENDPOINTS = "service_provider_endpoints"; public static final String OD_NAME_CATEGORY = "category"; @@ -140,6 +146,15 @@ public class XmlUtils { /** * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}. */ + public static Element getSingleChildElement( + Node parentEle, String tagName, Set<String> requiredStrings) + throws MalformedXmlException { + return getSingleChildElement(parentEle, tagName, requiredStrings.contains(tagName)); + } + + /** + * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}. + */ public static Element getSingleChildElement(Node parentEle, String tagName, boolean required) throws MalformedXmlException { String parentTagNameForErrorMsg = @@ -287,19 +302,36 @@ public class XmlUtils { } /** Gets a pipeline-split attribute. */ + public static List<String> getPipelineSplitAttr( + Element ele, String attrName, Set<String> requiredNames) throws MalformedXmlException { + return getPipelineSplitAttr(ele, attrName, requiredNames.contains(attrName)); + } + + /** Gets a pipeline-split attribute. */ public static List<String> getPipelineSplitAttr(Element ele, String attrName, boolean required) throws MalformedXmlException { List<String> list = Arrays.stream(ele.getAttribute(attrName).split("\\|")).collect(Collectors.toList()); - if ((list.isEmpty() || list.get(0).isEmpty()) && required) { - throw new MalformedXmlException( - String.format( - "Delimited string %s was required but missing, in %s.", - attrName, ele.getTagName())); + if ((list.isEmpty() || list.get(0).isEmpty())) { + if (required) { + throw new MalformedXmlException( + String.format( + "Delimited string %s was required but missing, in %s.", + attrName, ele.getTagName())); + } + return null; } return list; } + /** + * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}. + */ + public static Boolean getBoolAttr(Element ele, String attrName, Set<String> requiredStrings) + throws MalformedXmlException { + return getBoolAttr(ele, attrName, requiredStrings.contains(attrName)); + } + /** Gets a Boolean attribute. */ public static Boolean getBoolAttr(Element ele, String attrName, boolean required) throws MalformedXmlException { @@ -314,6 +346,12 @@ public class XmlUtils { } /** Gets a Boolean attribute. */ + public static Boolean getOdBoolEle(Element ele, String nameName, Set<String> requiredNames) + throws MalformedXmlException { + return getOdBoolEle(ele, nameName, requiredNames.contains(nameName)); + } + + /** Gets a Boolean attribute. */ public static Boolean getOdBoolEle(Element ele, String nameName, boolean required) throws MalformedXmlException { List<Element> boolEles = @@ -376,6 +414,12 @@ public class XmlUtils { } /** Gets an on-device String attribute. */ + public static String getOdStringEle(Element ele, String nameName, Set<String> requiredNames) + throws MalformedXmlException { + return getOdStringEle(ele, nameName, requiredNames.contains(nameName)); + } + + /** Gets an on-device String attribute. */ public static String getOdStringEle(Element ele, String nameName, boolean required) throws MalformedXmlException { List<Element> eles = @@ -404,6 +448,13 @@ public class XmlUtils { } /** Gets a OD Pbundle Element attribute with the specified name. */ + public static Element getOdPbundleWithName( + Element ele, String nameName, Set<String> requiredStrings) + throws MalformedXmlException { + return getOdPbundleWithName(ele, nameName, requiredStrings.contains(nameName)); + } + + /** Gets a OD Pbundle Element attribute with the specified name. */ public static Element getOdPbundleWithName(Element ele, String nameName, boolean required) throws MalformedXmlException { List<Element> eles = @@ -425,6 +476,14 @@ public class XmlUtils { return eles.get(0); } + /** + * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}. + */ + public static String getStringAttr(Element ele, String attrName, Set<String> requiredStrings) + throws MalformedXmlException { + return getStringAttr(ele, attrName, requiredStrings.contains(attrName)); + } + /** Gets a required String attribute. */ public static String getStringAttr(Element ele, String attrName) throws MalformedXmlException { return getStringAttr(ele, attrName, true); @@ -476,6 +535,13 @@ public class XmlUtils { /** Gets human-readable style String array. */ public static List<String> getHrItemsAsStrings( + Element parent, String elementName, Set<String> requiredNames) + throws MalformedXmlException { + return getHrItemsAsStrings(parent, elementName, requiredNames.contains(elementName)); + } + + /** Gets human-readable style String array. */ + public static List<String> getHrItemsAsStrings( Element parent, String elementName, boolean required) throws MalformedXmlException { List<Element> arrayEles = XmlUtils.getChildrenByTagName(parent, elementName); @@ -501,6 +567,12 @@ public class XmlUtils { } /** Gets on-device style String array. */ + public static List<String> getOdStringArray( + Element ele, String nameName, Set<String> requiredNames) throws MalformedXmlException { + return getOdStringArray(ele, nameName, requiredNames.contains(nameName)); + } + + /** Gets on-device style String array. */ public static List<String> getOdStringArray(Element ele, String nameName, boolean required) throws MalformedXmlException { List<Element> arrayEles = @@ -530,6 +602,73 @@ public class XmlUtils { return strs; } + /** Throws if extraneous child elements detected */ + public static void throwIfExtraneousChildrenHr(Element ele, Set<String> expectedChildNames) + throws MalformedXmlException { + var childEles = XmlUtils.asElementList(ele.getChildNodes()); + List<Element> extraneousEles = + childEles.stream() + .filter(e -> !expectedChildNames.contains(e.getTagName())) + .collect(Collectors.toList()); + if (!extraneousEles.isEmpty()) { + throw new MalformedXmlException( + String.format( + "Unexpected element(s) %s in %s.", + extraneousEles.stream() + .map(Element::getTagName) + .collect(Collectors.joining(",")), + ele.getTagName())); + } + } + + /** Throws if extraneous child elements detected */ + public static void throwIfExtraneousChildrenOd(Element ele, Set<String> expectedChildNames) + throws MalformedXmlException { + var allChildElements = XmlUtils.asElementList(ele.getChildNodes()); + List<Element> extraneousEles = + allChildElements.stream() + .filter( + e -> + !e.getAttribute(XmlUtils.OD_ATTR_NAME).isEmpty() + && !expectedChildNames.contains( + e.getAttribute(XmlUtils.OD_ATTR_NAME))) + .collect(Collectors.toList()); + if (!extraneousEles.isEmpty()) { + throw new MalformedXmlException( + String.format( + "Unexpected element(s) in %s: %s", + ele.getTagName(), + extraneousEles.stream() + .map( + e -> + String.format( + "%s name=%s", + e.getTagName(), + e.getAttribute(XmlUtils.OD_ATTR_NAME))) + .collect(Collectors.joining(",")))); + } + } + + /** Throws if extraneous attributes detected */ + public static void throwIfExtraneousAttributes(Element ele, Set<String> expectedAttrNames) + throws MalformedXmlException { + var attrs = ele.getAttributes(); + List<String> attrNames = new ArrayList<>(); + for (int i = 0; i < attrs.getLength(); i++) { + attrNames.add(attrs.item(i).getNodeName()); + } + List<String> extraneousAttrs = + attrNames.stream() + .filter(s -> !expectedAttrNames.contains(s)) + .collect(Collectors.toList()); + if (!extraneousAttrs.isEmpty()) { + throw new MalformedXmlException( + String.format( + "Unexpected attr(s) %s in %s.", + String.join(",", extraneousAttrs), ele.getTagName())); + } + } + /** * Utility method for making a List from one element, to support easier refactoring if needed. * For example, List.of() doesn't support null elements. @@ -537,4 +676,26 @@ public class XmlUtils { public static List<Element> listOf(Element e) { return Arrays.asList(e); } + + /** + * Gets the most recent version of fields in the mapping. This way when a new version is + * released, we only need to update the mappings that were modified. The rest will fall back to + * the most recent previous version. + */ + public static Set<String> getMostRecentVersion( + Map<Long, Set<String>> versionToFieldsMapping, long version) + throws MalformedXmlException { + long bestVersion = 0; + Set<String> bestSet = null; + for (Map.Entry<Long, Set<String>> entry : versionToFieldsMapping.entrySet()) { + if (entry.getKey() > bestVersion && entry.getKey() <= version) { + bestVersion = entry.getKey(); + bestSet = entry.getValue(); + } + } + if (bestSet == null) { + throw new MalformedXmlException("Unexpected version: " + version); + } + return bestSet; + } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java index 5d1d45a9a29b..262f76d52c90 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java @@ -41,6 +41,18 @@ public class AslgenTests { /** Logic for setting up tests (empty if not yet needed). */ public static void main(String[] params) throws Exception {} + @Test + public void testValidOd() throws Exception { + System.out.println("start testing valid od."); + Path odPath = Paths.get(VALID_MAPPINGS_PATH, "general-v1", OD_XML_FILENAME); + InputStream odStream = getClass().getClassLoader().getResourceAsStream(odPath.toString()); + String odContents = + TestUtils.getFormattedXml( + new String(odStream.readAllBytes(), StandardCharsets.UTF_8), false); + AndroidSafetyLabel unusedAsl = + AslConverter.readFromString(odContents, AslConverter.Format.ON_DEVICE); + } + /** Tests valid mappings between HR and OD. */ @Test public void testValidMappings() throws Exception { diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java index 61a78232801c..283ccbc44791 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java @@ -16,12 +16,18 @@ package com.android.asllib.marshallable; +import static org.junit.Assert.assertThrows; + import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.w3c.dom.Element; + +import java.nio.file.Paths; @RunWith(JUnit4.class) public class AndroidSafetyLabelTest { @@ -66,47 +72,49 @@ public class AndroidSafetyLabelTest { testOdToHrAndroidSafetyLabel(WITH_SAFETY_LABELS_FILE_NAME); } - /** Test for android safety label with system app safety label. */ - @Test - public void testAndroidSafetyLabelWithSystemAppSafetyLabel() throws Exception { - System.out.println("starting testAndroidSafetyLabelWithSystemAppSafetyLabel."); - testHrToOdAndroidSafetyLabel(WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME); - testOdToHrAndroidSafetyLabel(WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME); - } - - /** Test for android safety label with transparency info. */ - @Test - public void testAndroidSafetyLabelWithTransparencyInfo() throws Exception { - System.out.println("starting testAndroidSafetyLabelWithTransparencyInfo."); - testHrToOdAndroidSafetyLabel(WITH_TRANSPARENCY_INFO_FILE_NAME); - testOdToHrAndroidSafetyLabel(WITH_TRANSPARENCY_INFO_FILE_NAME); - } - private void hrToOdExpectException(String fileName) { - TestUtils.hrToOdExpectException( - new AndroidSafetyLabelFactory(), ANDROID_SAFETY_LABEL_HR_PATH, fileName); + assertThrows( + MalformedXmlException.class, + () -> { + new AndroidSafetyLabelFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(ANDROID_SAFETY_LABEL_HR_PATH, fileName))); + }); } private void odToHrExpectException(String fileName) { - TestUtils.odToHrExpectException( - new AndroidSafetyLabelFactory(), ANDROID_SAFETY_LABEL_OD_PATH, fileName); + assertThrows( + MalformedXmlException.class, + () -> { + new AndroidSafetyLabelFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(ANDROID_SAFETY_LABEL_OD_PATH, fileName))); + }); } private void testHrToOdAndroidSafetyLabel(String fileName) throws Exception { - TestUtils.testHrToOd( - TestUtils.document(), - new AndroidSafetyLabelFactory(), - ANDROID_SAFETY_LABEL_HR_PATH, - ANDROID_SAFETY_LABEL_OD_PATH, - fileName); + var doc = TestUtils.document(); + AndroidSafetyLabel asl = + new AndroidSafetyLabelFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(ANDROID_SAFETY_LABEL_HR_PATH, fileName))); + Element aslEle = asl.toOdDomElement(doc); + doc.appendChild(aslEle); + TestUtils.testFormatToFormat(doc, Paths.get(ANDROID_SAFETY_LABEL_OD_PATH, fileName)); } private void testOdToHrAndroidSafetyLabel(String fileName) throws Exception { - TestUtils.testOdToHr( - TestUtils.document(), - new AndroidSafetyLabelFactory(), - ANDROID_SAFETY_LABEL_OD_PATH, - ANDROID_SAFETY_LABEL_HR_PATH, - fileName); + var doc = TestUtils.document(); + AndroidSafetyLabel asl = + new AndroidSafetyLabelFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(ANDROID_SAFETY_LABEL_OD_PATH, fileName))); + Element aslEle = asl.toHrDomElement(doc); + doc.appendChild(aslEle); + TestUtils.testFormatToFormat(doc, Paths.get(ANDROID_SAFETY_LABEL_HR_PATH, fileName)); } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java index d823c482adfe..7806061f00bb 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java @@ -26,12 +26,14 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.w3c.dom.Element; import java.nio.file.Paths; import java.util.List; @RunWith(JUnit4.class) public class AppInfoTest { + private static final long DEFAULT_VERSION = 2L; private static final String APP_INFO_HR_PATH = "com/android/asllib/appinfo/hr"; private static final String APP_INFO_OD_PATH = "com/android/asllib/appinfo/od"; public static final List<String> REQUIRED_FIELD_NAMES = @@ -45,7 +47,24 @@ public class AppInfoTest { public static final List<String> OPTIONAL_FIELD_NAMES = List.of(); public static final List<String> OPTIONAL_FIELD_NAMES_OD = List.of(); + public static final List<String> REQUIRED_FIELD_NAMES_OD_V1 = + List.of( + "title", + "description", + "contains_ads", + "obey_aps", + "ads_fingerprinting", + "security_fingerprinting", + "privacy_policy", + "security_endpoints", + "first_party_endpoints", + "service_provider_endpoints", + "category"); + public static final List<String> OPTIONAL_FIELD_NAMES_OD_V1 = List.of("website", "email"); + private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml"; + private static final String ALL_FIELDS_VALID_V1_FILE_NAME = "all-fields-valid-v1.xml"; + public static final String UNRECOGNIZED_V1_FILE_NAME = "unrecognized-v1.xml"; /** Logic for setting up tests (empty if not yet needed). */ public static void main(String[] params) throws Exception {} @@ -63,6 +82,61 @@ public class AppInfoTest { testOdToHrAppInfo(ALL_FIELDS_VALID_FILE_NAME); } + /** Test for all fields valid v1. */ + @Test + public void testAllFieldsValidV1() throws Exception { + System.out.println("starting testAllFieldsValidV1."); + new AppInfoFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(APP_INFO_OD_PATH, ALL_FIELDS_VALID_V1_FILE_NAME)), + 1L); + } + + /** Test for unrecognized field v1. */ + @Test + public void testUnrecognizedFieldV1() throws Exception { + System.out.println("starting testUnrecognizedFieldV1."); + assertThrows( + MalformedXmlException.class, + () -> + new AppInfoFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get( + APP_INFO_OD_PATH, + UNRECOGNIZED_V1_FILE_NAME)), + 1L)); + } + + /** Tests missing required fields fails, V1. */ + @Test + public void testMissingRequiredFieldsOdV1() throws Exception { + for (String reqField : REQUIRED_FIELD_NAMES_OD_V1) { + System.out.println("testing missing required field od v1: " + reqField); + var appInfoEle = + TestUtils.getElementFromResource( + Paths.get(APP_INFO_OD_PATH, ALL_FIELDS_VALID_V1_FILE_NAME)); + TestUtils.removeOdChildEleWithName(appInfoEle, reqField); + assertThrows( + MalformedXmlException.class, + () -> new AppInfoFactory().createFromOdElement(appInfoEle, 1L)); + } + } + + /** Tests missing optional fields passes, V1. */ + @Test + public void testMissingOptionalFieldsOdV1() throws Exception { + for (String optField : OPTIONAL_FIELD_NAMES_OD_V1) { + System.out.println("testing missing optional field od v1: " + optField); + var appInfoEle = + TestUtils.getElementFromResource( + Paths.get(APP_INFO_OD_PATH, ALL_FIELDS_VALID_V1_FILE_NAME)); + TestUtils.removeOdChildEleWithName(appInfoEle, optField); + new AppInfoFactory().createFromOdElement(appInfoEle, 1L); + } + } + /** Tests missing required fields fails. */ @Test public void testMissingRequiredFields() throws Exception { @@ -70,24 +144,24 @@ public class AppInfoTest { for (String reqField : REQUIRED_FIELD_NAMES) { System.out.println("testing missing required field hr: " + reqField); var appInfoEle = - TestUtils.getElementsFromResource( + TestUtils.getElementFromResource( Paths.get(APP_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); - appInfoEle.get(0).removeAttribute(reqField); + appInfoEle.removeAttribute(reqField); assertThrows( MalformedXmlException.class, - () -> new AppInfoFactory().createFromHrElements(appInfoEle)); + () -> new AppInfoFactory().createFromHrElement(appInfoEle, DEFAULT_VERSION)); } for (String reqField : REQUIRED_FIELD_NAMES_OD) { System.out.println("testing missing required field od: " + reqField); var appInfoEle = - TestUtils.getElementsFromResource( + TestUtils.getElementFromResource( Paths.get(APP_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); - TestUtils.removeOdChildEleWithName(appInfoEle.get(0), reqField); + TestUtils.removeOdChildEleWithName(appInfoEle, reqField); assertThrows( MalformedXmlException.class, - () -> new AppInfoFactory().createFromOdElements(appInfoEle)); + () -> new AppInfoFactory().createFromOdElement(appInfoEle, DEFAULT_VERSION)); } } @@ -98,24 +172,24 @@ public class AppInfoTest { for (String reqChildName : REQUIRED_CHILD_NAMES) { System.out.println("testing missing required child hr: " + reqChildName); var appInfoEle = - TestUtils.getElementsFromResource( + TestUtils.getElementFromResource( Paths.get(APP_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); - var child = XmlUtils.getChildrenByTagName(appInfoEle.get(0), reqChildName).get(0); - appInfoEle.get(0).removeChild(child); + var child = XmlUtils.getChildrenByTagName(appInfoEle, reqChildName).get(0); + appInfoEle.removeChild(child); assertThrows( MalformedXmlException.class, - () -> new AppInfoFactory().createFromHrElements(appInfoEle)); + () -> new AppInfoFactory().createFromHrElement(appInfoEle, DEFAULT_VERSION)); } for (String reqField : REQUIRED_CHILD_NAMES_OD) { System.out.println("testing missing required child od: " + reqField); var appInfoEle = - TestUtils.getElementsFromResource( + TestUtils.getElementFromResource( Paths.get(APP_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); - TestUtils.removeOdChildEleWithName(appInfoEle.get(0), reqField); + TestUtils.removeOdChildEleWithName(appInfoEle, reqField); assertThrows( MalformedXmlException.class, - () -> new AppInfoFactory().createFromOdElements(appInfoEle)); + () -> new AppInfoFactory().createFromOdElement(appInfoEle, DEFAULT_VERSION)); } } @@ -124,38 +198,51 @@ public class AppInfoTest { public void testMissingOptionalFields() throws Exception { for (String optField : OPTIONAL_FIELD_NAMES) { var ele = - TestUtils.getElementsFromResource( + TestUtils.getElementFromResource( Paths.get(APP_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); - ele.get(0).removeAttribute(optField); - AppInfo appInfo = new AppInfoFactory().createFromHrElements(ele); - appInfo.toOdDomElements(TestUtils.document()); + ele.removeAttribute(optField); + AppInfo appInfo = new AppInfoFactory().createFromHrElement(ele, DEFAULT_VERSION); + appInfo.toOdDomElement(TestUtils.document()); } for (String optField : OPTIONAL_FIELD_NAMES_OD) { var ele = - TestUtils.getElementsFromResource( + TestUtils.getElementFromResource( Paths.get(APP_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); - TestUtils.removeOdChildEleWithName(ele.get(0), optField); - AppInfo appInfo = new AppInfoFactory().createFromOdElements(ele); - appInfo.toHrDomElements(TestUtils.document()); + TestUtils.removeOdChildEleWithName(ele, optField); + AppInfo appInfo = new AppInfoFactory().createFromOdElement(ele, DEFAULT_VERSION); + appInfo.toHrDomElement(TestUtils.document()); } } private void testHrToOdAppInfo(String fileName) throws Exception { - TestUtils.testHrToOd( - TestUtils.document(), - new AppInfoFactory(), - APP_INFO_HR_PATH, - APP_INFO_OD_PATH, - fileName); + var doc = TestUtils.document(); + AppInfo appInfo = + new AppInfoFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(APP_INFO_HR_PATH, fileName)), + DEFAULT_VERSION); + Element appInfoEle = appInfo.toOdDomElement(doc); + doc.appendChild(appInfoEle); + TestUtils.testFormatToFormat(doc, Paths.get(APP_INFO_OD_PATH, fileName)); } + private void testOdToHrAppInfo(String fileName) throws Exception { - TestUtils.testOdToHr( - TestUtils.document(), - new AppInfoFactory(), - APP_INFO_OD_PATH, - APP_INFO_HR_PATH, - fileName); + testOdToHrAppInfo(fileName, DEFAULT_VERSION); + } + + private void testOdToHrAppInfo(String fileName, long version) throws Exception { + var doc = TestUtils.document(); + AppInfo appInfo = + new AppInfoFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(APP_INFO_OD_PATH, fileName)), + version); + Element appInfoEle = appInfo.toHrDomElement(doc); + doc.appendChild(appInfoEle); + TestUtils.testFormatToFormat(doc, Paths.get(APP_INFO_HR_PATH, fileName)); } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java index ff4374166dd3..b557fea9572b 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java @@ -16,15 +16,27 @@ package com.android.asllib.marshallable; +import static org.junit.Assert.assertThrows; + import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.nio.file.Paths; + +import javax.xml.parsers.ParserConfigurationException; @RunWith(JUnit4.class) public class DataLabelsTest { + private static final long DEFAULT_VERSION = 2L; + private static final String DATA_LABELS_HR_PATH = "com/android/asllib/datalabels/hr"; private static final String DATA_LABELS_OD_PATH = "com/android/asllib/datalabels/od"; @@ -297,29 +309,43 @@ public class DataLabelsTest { odToHrExpectException(PERSONAL_EMPTY_PURPOSE_FILE_NAME); } - private void hrToOdExpectException(String fileName) { - TestUtils.hrToOdExpectException(new DataLabelsFactory(), DATA_LABELS_HR_PATH, fileName); + private void hrToOdExpectException(String fileName) + throws ParserConfigurationException, IOException, SAXException { + var ele = TestUtils.getElementFromResource(Paths.get(DATA_LABELS_HR_PATH, fileName)); + assertThrows( + MalformedXmlException.class, + () -> new DataLabelsFactory().createFromHrElement(ele)); } - private void odToHrExpectException(String fileName) { - TestUtils.odToHrExpectException(new DataLabelsFactory(), DATA_LABELS_OD_PATH, fileName); + private void odToHrExpectException(String fileName) + throws ParserConfigurationException, IOException, SAXException { + var ele = TestUtils.getElementFromResource(Paths.get(DATA_LABELS_OD_PATH, fileName)); + assertThrows( + MalformedXmlException.class, + () -> new DataLabelsFactory().createFromOdElement(ele)); } private void testHrToOdDataLabels(String fileName) throws Exception { - TestUtils.testHrToOd( - TestUtils.document(), - new DataLabelsFactory(), - DATA_LABELS_HR_PATH, - DATA_LABELS_OD_PATH, - fileName); + var doc = TestUtils.document(); + DataLabels dataLabels = + new DataLabelsFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(DATA_LABELS_HR_PATH, fileName))); + Element resultingEle = dataLabels.toOdDomElement(doc); + doc.appendChild(resultingEle); + TestUtils.testFormatToFormat(doc, Paths.get(DATA_LABELS_OD_PATH, fileName)); } private void testOdToHrDataLabels(String fileName) throws Exception { - TestUtils.testOdToHr( - TestUtils.document(), - new DataLabelsFactory(), - DATA_LABELS_OD_PATH, - DATA_LABELS_HR_PATH, - fileName); + var doc = TestUtils.document(); + DataLabels dataLabels = + new DataLabelsFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(DATA_LABELS_OD_PATH, fileName))); + Element resultingEle = dataLabels.toHrDomElement(doc); + doc.appendChild(resultingEle); + TestUtils.testFormatToFormat(doc, Paths.get(DATA_LABELS_HR_PATH, fileName)); } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java index 19d1626f7054..7cd510f0ddfc 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java @@ -16,15 +16,27 @@ package com.android.asllib.marshallable; +import static org.junit.Assert.assertThrows; + import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.nio.file.Paths; + +import javax.xml.parsers.ParserConfigurationException; @RunWith(JUnit4.class) public class SafetyLabelsTest { + private static final long DEFAULT_VERSION = 2L; + private static final String SAFETY_LABELS_HR_PATH = "com/android/asllib/safetylabels/hr"; private static final String SAFETY_LABELS_OD_PATH = "com/android/asllib/safetylabels/od"; @@ -52,29 +64,51 @@ public class SafetyLabelsTest { testOdToHrSafetyLabels(WITH_DATA_LABELS_FILE_NAME); } - private void hrToOdExpectException(String fileName) { - TestUtils.hrToOdExpectException(new SafetyLabelsFactory(), SAFETY_LABELS_HR_PATH, fileName); + private void hrToOdExpectException(String fileName) + throws ParserConfigurationException, IOException, SAXException { + var safetyLabelsEle = + TestUtils.getElementFromResource(Paths.get(SAFETY_LABELS_HR_PATH, fileName)); + assertThrows( + MalformedXmlException.class, + () -> + new SafetyLabelsFactory() + .createFromHrElement(safetyLabelsEle, DEFAULT_VERSION)); } - private void odToHrExpectException(String fileName) { - TestUtils.odToHrExpectException(new SafetyLabelsFactory(), SAFETY_LABELS_OD_PATH, fileName); + private void odToHrExpectException(String fileName) + throws ParserConfigurationException, IOException, SAXException { + var safetyLabelsEle = + TestUtils.getElementFromResource(Paths.get(SAFETY_LABELS_OD_PATH, fileName)); + assertThrows( + MalformedXmlException.class, + () -> + new SafetyLabelsFactory() + .createFromOdElement(safetyLabelsEle, DEFAULT_VERSION)); } private void testHrToOdSafetyLabels(String fileName) throws Exception { - TestUtils.testHrToOd( - TestUtils.document(), - new SafetyLabelsFactory(), - SAFETY_LABELS_HR_PATH, - SAFETY_LABELS_OD_PATH, - fileName); + var doc = TestUtils.document(); + SafetyLabels safetyLabels = + new SafetyLabelsFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(SAFETY_LABELS_HR_PATH, fileName)), + DEFAULT_VERSION); + Element appInfoEle = safetyLabels.toOdDomElement(doc); + doc.appendChild(appInfoEle); + TestUtils.testFormatToFormat(doc, Paths.get(SAFETY_LABELS_OD_PATH, fileName)); } private void testOdToHrSafetyLabels(String fileName) throws Exception { - TestUtils.testOdToHr( - TestUtils.document(), - new SafetyLabelsFactory(), - SAFETY_LABELS_OD_PATH, - SAFETY_LABELS_HR_PATH, - fileName); + var doc = TestUtils.document(); + SafetyLabels safetyLabels = + new SafetyLabelsFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(SAFETY_LABELS_OD_PATH, fileName)), + DEFAULT_VERSION); + Element appInfoEle = safetyLabels.toHrDomElement(doc); + doc.appendChild(appInfoEle); + TestUtils.testFormatToFormat(doc, Paths.get(SAFETY_LABELS_HR_PATH, fileName)); } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java index 87d3e4499f5c..9dcc6529969e 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java @@ -16,15 +16,27 @@ package com.android.asllib.marshallable; +import static org.junit.Assert.assertThrows; + import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.nio.file.Paths; + +import javax.xml.parsers.ParserConfigurationException; @RunWith(JUnit4.class) public class SystemAppSafetyLabelTest { + private static final long DEFAULT_VERSION = 2L; + private static final String SYSTEM_APP_SAFETY_LABEL_HR_PATH = "com/android/asllib/systemappsafetylabel/hr"; private static final String SYSTEM_APP_SAFETY_LABEL_OD_PATH = @@ -57,31 +69,49 @@ public class SystemAppSafetyLabelTest { odToHrExpectException(MISSING_BOOL_FILE_NAME); } - private void hrToOdExpectException(String fileName) { - TestUtils.hrToOdExpectException( - new SystemAppSafetyLabelFactory(), SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName); + private void hrToOdExpectException(String fileName) + throws ParserConfigurationException, IOException, SAXException { + var ele = + TestUtils.getElementFromResource( + Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName)); + assertThrows( + MalformedXmlException.class, + () -> new SystemAppSafetyLabelFactory().createFromHrElement(ele, DEFAULT_VERSION)); } - private void odToHrExpectException(String fileName) { - TestUtils.odToHrExpectException( - new SystemAppSafetyLabelFactory(), SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName); + private void odToHrExpectException(String fileName) + throws ParserConfigurationException, IOException, SAXException { + var ele = + TestUtils.getElementFromResource( + Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName)); + assertThrows( + MalformedXmlException.class, + () -> new SystemAppSafetyLabelFactory().createFromOdElement(ele, DEFAULT_VERSION)); } private void testHrToOdSystemAppSafetyLabel(String fileName) throws Exception { - TestUtils.testHrToOd( - TestUtils.document(), - new SystemAppSafetyLabelFactory(), - SYSTEM_APP_SAFETY_LABEL_HR_PATH, - SYSTEM_APP_SAFETY_LABEL_OD_PATH, - fileName); + var doc = TestUtils.document(); + SystemAppSafetyLabel systemAppSafetyLabel = + new SystemAppSafetyLabelFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName)), + DEFAULT_VERSION); + Element resultingEle = systemAppSafetyLabel.toOdDomElement(doc); + doc.appendChild(resultingEle); + TestUtils.testFormatToFormat(doc, Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName)); } private void testOdToHrSystemAppSafetyLabel(String fileName) throws Exception { - TestUtils.testOdToHr( - TestUtils.document(), - new SystemAppSafetyLabelFactory(), - SYSTEM_APP_SAFETY_LABEL_OD_PATH, - SYSTEM_APP_SAFETY_LABEL_HR_PATH, - fileName); + var doc = TestUtils.document(); + SystemAppSafetyLabel systemAppSafetyLabel = + new SystemAppSafetyLabelFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName)), + DEFAULT_VERSION); + Element resultingEle = systemAppSafetyLabel.toHrDomElement(doc); + doc.appendChild(resultingEle); + TestUtils.testFormatToFormat(doc, Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName)); } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java index 8a0b35eac7c8..6547fb952944 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java @@ -22,9 +22,14 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.w3c.dom.Element; + +import java.nio.file.Paths; @RunWith(JUnit4.class) public class TransparencyInfoTest { + private static final long DEFAULT_VERSION = 2L; + private static final String TRANSPARENCY_INFO_HR_PATH = "com/android/asllib/transparencyinfo/hr"; private static final String TRANSPARENCY_INFO_OD_PATH = @@ -45,20 +50,28 @@ public class TransparencyInfoTest { } private void testHrToOdTransparencyInfo(String fileName) throws Exception { - TestUtils.testHrToOd( - TestUtils.document(), - new TransparencyInfoFactory(), - TRANSPARENCY_INFO_HR_PATH, - TRANSPARENCY_INFO_OD_PATH, - fileName); + var doc = TestUtils.document(); + TransparencyInfo transparencyInfo = + new TransparencyInfoFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(TRANSPARENCY_INFO_HR_PATH, fileName)), + DEFAULT_VERSION); + Element resultingEle = transparencyInfo.toOdDomElement(doc); + doc.appendChild(resultingEle); + TestUtils.testFormatToFormat(doc, Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)); } private void testOdToHrTransparencyInfo(String fileName) throws Exception { - TestUtils.testOdToHr( - TestUtils.document(), - new TransparencyInfoFactory(), - TRANSPARENCY_INFO_OD_PATH, - TRANSPARENCY_INFO_HR_PATH, - fileName); + var doc = TestUtils.document(); + TransparencyInfo transparencyInfo = + new TransparencyInfoFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)), + DEFAULT_VERSION); + Element resultingEle = transparencyInfo.toHrDomElement(doc); + doc.appendChild(resultingEle); + TestUtils.testFormatToFormat(doc, Paths.get(TRANSPARENCY_INFO_HR_PATH, fileName)); } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java index ea90993e0785..f8ef40a9c509 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java @@ -17,11 +17,8 @@ package com.android.asllib.testutils; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; import com.android.asllib.marshallable.AslMarshallable; -import com.android.asllib.marshallable.AslMarshallableFactory; -import com.android.asllib.util.MalformedXmlException; import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; @@ -36,7 +33,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import java.util.Optional; @@ -60,7 +56,19 @@ public class TestUtils { } /** Gets List of Element from a path to an existing Resource. */ - public static List<Element> getElementsFromResource(Path filePath) + public static Element getElementFromResource(Path filePath) + throws ParserConfigurationException, IOException, SAXException { + String str = readStrFromResource(filePath); + InputStream stream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + Document document = factory.newDocumentBuilder().parse(stream); + return document.getDocumentElement(); + } + + /** Gets List of Element from a path to an existing Resource. */ + public static List<Element> getChildElementsFromResource(Path filePath) throws ParserConfigurationException, IOException, SAXException { String str = readStrFromResource(filePath); InputStream stream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); @@ -69,16 +77,13 @@ public class TestUtils { factory.setNamespaceAware(true); Document document = factory.newDocumentBuilder().parse(stream); Element root = document.getDocumentElement(); - if (root.getTagName().equals(HOLDER_TAG_NAME)) { - String tagName = - XmlUtils.asElementList(root.getChildNodes()).stream() - .findFirst() - .get() - .getTagName(); - return XmlUtils.getChildrenByTagName(root, tagName); - } else { - return List.of(root); - } + + String tagName = + XmlUtils.asElementList(root.getChildNodes()).stream() + .findFirst() + .get() + .getTagName(); + return XmlUtils.getChildrenByTagName(root, tagName); } /** Reads a Document into a String. */ @@ -130,86 +135,13 @@ public class TestUtils { return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); } - /** Helper for testing human-readable to on-device conversion expecting exception */ - public static <T extends AslMarshallable> void hrToOdExpectException( - AslMarshallableFactory<T> factory, String hrFolderPath, String fileName) { - assertThrows( - MalformedXmlException.class, - () -> { - factory.createFromHrElements( - TestUtils.getElementsFromResource(Paths.get(hrFolderPath, fileName))); - }); - } - - /** Helper for testing on-device to human-readable conversion expecting exception */ - public static <T extends AslMarshallable> void odToHrExpectException( - AslMarshallableFactory<T> factory, String odFolderPath, String fileName) { - assertThrows( - MalformedXmlException.class, - () -> { - factory.createFromOdElements( - TestUtils.getElementsFromResource(Paths.get(odFolderPath, fileName))); - }); - } - - /** Helper for testing human-readable to on-device conversion */ - public static <T extends AslMarshallable> void testHrToOd( - Document doc, - AslMarshallableFactory<T> factory, - String hrFolderPath, - String odFolderPath, - String fileName) - throws Exception { - testFormatToFormat(doc, factory, hrFolderPath, odFolderPath, fileName, true); - } - - /** Helper for testing on-device to human-readable conversion */ - public static <T extends AslMarshallable> void testOdToHr( - Document doc, - AslMarshallableFactory<T> factory, - String odFolderPath, - String hrFolderPath, - String fileName) - throws Exception { - testFormatToFormat(doc, factory, odFolderPath, hrFolderPath, fileName, false); - } - /** Helper for testing format to format conversion */ - private static <T extends AslMarshallable> void testFormatToFormat( - Document doc, - AslMarshallableFactory<T> factory, - String inFolderPath, - String outFolderPath, - String fileName, - boolean hrToOd) - throws Exception { - AslMarshallable marshallable = - hrToOd - ? factory.createFromHrElements( - TestUtils.getElementsFromResource( - Paths.get(inFolderPath, fileName))) - : factory.createFromOdElements( - TestUtils.getElementsFromResource( - Paths.get(inFolderPath, fileName))); - - List<Element> elements = - hrToOd ? marshallable.toOdDomElements(doc) : marshallable.toHrDomElements(doc); - if (elements.isEmpty()) { - throw new IllegalStateException("elements was empty."); - } else if (elements.size() == 1) { - doc.appendChild(elements.get(0)); - } else { - Element root = doc.createElement(TestUtils.HOLDER_TAG_NAME); - for (var child : elements) { - root.appendChild(child); - } - doc.appendChild(root); - } + public static <T extends AslMarshallable> void testFormatToFormat( + Document doc, Path expectedOutPath) throws Exception { String converted = TestUtils.getFormattedXml(TestUtils.docToStr(doc, true), true); System.out.println("Converted: " + converted); String expectedOutContents = - TestUtils.getFormattedXml( - TestUtils.readStrFromResource(Paths.get(outFolderPath, fileName)), true); + TestUtils.getFormattedXml(TestUtils.readStrFromResource(expectedOutPath), true); System.out.println("Expected: " + expectedOutContents); assertEquals(expectedOutContents, converted); } diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/missing-version.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/missing-version.xml index ec0cd702fd43..7478e39ce366 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/missing-version.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/missing-version.xml @@ -1,3 +1,17 @@ <app-metadata-bundles> - + <system-app-safety-label declaration="true"> + </system-app-safety-label> + <transparency-info> + <app-info + apsCompliant="false" + privacyPolicy="www.example.com" developerId="dev1" applicationId="app1"> + <first-party-endpoints> + <item>url1</item> + </first-party-endpoints> + <service-provider-endpoints> + <item>url55</item> + <item>url56</item> + </service-provider-endpoints> + </app-info> + </transparency-info> </app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/valid-empty.xml index 19bfd826f770..587c49add47e 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/valid-empty.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/valid-empty.xml @@ -1 +1,17 @@ -<app-metadata-bundles version="123456"></app-metadata-bundles>
\ No newline at end of file +<app-metadata-bundles version="2"> + <system-app-safety-label declaration="true"> + </system-app-safety-label> + <transparency-info> + <app-info + apsCompliant="false" + privacyPolicy="www.example.com" developerId="dev1" applicationId="app1"> + <first-party-endpoints> + <item>url1</item> + </first-party-endpoints> + <service-provider-endpoints> + <item>url55</item> + <item>url56</item> + </service-provider-endpoints> + </app-info> + </transparency-info> +</app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-safety-labels.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-safety-labels.xml index 03e71d28f4cc..9cfb8bc4d5da 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-safety-labels.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-safety-labels.xml @@ -1,4 +1,19 @@ -<app-metadata-bundles version="123456"> +<app-metadata-bundles version="2"> <safety-labels> </safety-labels> + <system-app-safety-label declaration="true"> + </system-app-safety-label> + <transparency-info> + <app-info + apsCompliant="false" + privacyPolicy="www.example.com" developerId="dev1" applicationId="app1"> + <first-party-endpoints> + <item>url1</item> + </first-party-endpoints> + <service-provider-endpoints> + <item>url55</item> + <item>url56</item> + </service-provider-endpoints> + </app-info> + </transparency-info> </app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-system-app-safety-label.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-system-app-safety-label.xml deleted file mode 100644 index afb048632bc6..000000000000 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-system-app-safety-label.xml +++ /dev/null @@ -1,4 +0,0 @@ -<app-metadata-bundles version="123456"> -<system-app-safety-label declaration="true"> -</system-app-safety-label> -</app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-transparency-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-transparency-info.xml deleted file mode 100644 index a00ef6565cf4..000000000000 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-transparency-info.xml +++ /dev/null @@ -1,15 +0,0 @@ -<app-metadata-bundles version="123456"> - <transparency-info> - <app-info - apsCompliant="false" - privacyPolicy="www.example.com"> - <first-party-endpoints> - <item>url1</item> - </first-party-endpoints> - <service-provider-endpoints> - <item>url55</item> - <item>url56</item> - </service-provider-endpoints> - </app-info> - </transparency-info> -</app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/missing-version.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/missing-version.xml index 1aa3aa94ca6d..9adfa98f05a3 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/missing-version.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/missing-version.xml @@ -1,2 +1,20 @@ <bundle> + <pbundle_as_map name="system_app_safety_label"> + <boolean name="declaration" value="true"/> + </pbundle_as_map> + <pbundle_as_map name="transparency_info"> + <pbundle_as_map name="app_info"> + <boolean name="aps_compliant" value="false"/> + <string name="privacy_policy" value="www.example.com"/> + <string-array name="first_party_endpoints" num="1"> + <item value="url1"/> + </string-array> + <string-array name="service_provider_endpoints" num="2"> + <item value="url55"/> + <item value="url56"/> + </string-array> + <string name="developer_id" value="dev1"/> + <string name="application_id" value="app1"/> + </pbundle_as_map> + </pbundle_as_map> </bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-empty.xml index 37bdfad4065f..7a4c82a9e65f 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-empty.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-empty.xml @@ -1,3 +1,21 @@ <bundle> - <long name="version" value="123456"/> + <long name="version" value="2"/> + <pbundle_as_map name="system_app_safety_label"> + <boolean name="declaration" value="true"/> + </pbundle_as_map> + <pbundle_as_map name="transparency_info"> + <pbundle_as_map name="app_info"> + <string name="privacy_policy" value="www.example.com"/> + <string-array name="first_party_endpoints" num="1"> + <item value="url1"/> + </string-array> + <string-array name="service_provider_endpoints" num="2"> + <item value="url55"/> + <item value="url56"/> + </string-array> + <boolean name="aps_compliant" value="false"/> + <string name="developer_id" value="dev1"/> + <string name="application_id" value="app1"/> + </pbundle_as_map> + </pbundle_as_map> </bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-safety-labels.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-safety-labels.xml index f00fb26bf1ee..3a3e5d383ca9 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-safety-labels.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-safety-labels.xml @@ -1,5 +1,22 @@ <bundle> - <long name="version" value="123456"/> - <pbundle_as_map name="safety_labels"> + <long name="version" value="2"/> + <pbundle_as_map name="safety_labels"/> + <pbundle_as_map name="system_app_safety_label"> + <boolean name="declaration" value="true"/> </pbundle_as_map> -</bundle> + <pbundle_as_map name="transparency_info"> + <pbundle_as_map name="app_info"> + <string name="privacy_policy" value="www.example.com"/> + <string-array name="first_party_endpoints" num="1"> + <item value="url1"/> + </string-array> + <string-array name="service_provider_endpoints" num="2"> + <item value="url55"/> + <item value="url56"/> + </string-array> + <boolean name="aps_compliant" value="false"/> + <string name="developer_id" value="dev1"/> + <string name="application_id" value="app1"/> + </pbundle_as_map> + </pbundle_as_map> +</bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-system-app-safety-label.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-system-app-safety-label.xml deleted file mode 100644 index e8640c4f0934..000000000000 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-system-app-safety-label.xml +++ /dev/null @@ -1,6 +0,0 @@ -<bundle> - <long name="version" value="123456"/> - <pbundle_as_map name="system_app_safety_label"> - <boolean name="declaration" value="true"/> - </pbundle_as_map> -</bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-transparency-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-transparency-info.xml deleted file mode 100644 index d0c8668564bd..000000000000 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-transparency-info.xml +++ /dev/null @@ -1,16 +0,0 @@ -<bundle> - <long name="version" value="123456"/> - <pbundle_as_map name="transparency_info"> - <pbundle_as_map name="app_info"> - <boolean name="aps_compliant" value="false"/> - <string name="privacy_policy" value="www.example.com"/> - <string-array name="first_party_endpoints" num="1"> - <item value="url1"/> - </string-array> - <string-array name="service_provider_endpoints" num="2"> - <item value="url55"/> - <item value="url56"/> - </string-array> - </pbundle_as_map> - </pbundle_as_map> -</bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/hr/all-fields-valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/hr/all-fields-valid.xml index 0d15efc4eac3..306e01533731 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/hr/all-fields-valid.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/hr/all-fields-valid.xml @@ -1,6 +1,6 @@ <app-info apsCompliant="false" - privacyPolicy="www.example.com"> + privacyPolicy="www.example.com" developerId="dev1" applicationId="app1"> <first-party-endpoints> <item>url1</item> </first-party-endpoints> diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid-v1.xml new file mode 100644 index 000000000000..b026cf33e5f0 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid-v1.xml @@ -0,0 +1,24 @@ +<pbundle_as_map name="app_info"> + <string name="title" value="beervision"/> + <string name="description" value="a beer app"/> + <boolean name="contains_ads" value="true"/> + <boolean name="obey_aps" value="false"/> + <boolean name="ads_fingerprinting" value="false"/> + <boolean name="security_fingerprinting" value="false"/> + <string name="privacy_policy" value="www.example.com"/> + <string-array name="security_endpoints" num="3"> + <item value="url1"/> + <item value="url2"/> + <item value="url3"/> + </string-array> + <string-array name="first_party_endpoints" num="1"> + <item value="url1"/> + </string-array> + <string-array name="service_provider_endpoints" num="2"> + <item value="url55"/> + <item value="url56"/> + </string-array> + <string name="category" value="Food and drink"/> + <string name="email" value="max@maxloh.com"/> + <string name="website" value="www.example.com"/> +</pbundle_as_map> diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid.xml index bce51799c7ff..7aae4a715b79 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid.xml @@ -1,6 +1,5 @@ <pbundle_as_map name="app_info"> - <boolean name="aps_compliant" value="false"/> <string name="privacy_policy" value="www.example.com"/> <string-array name="first_party_endpoints" num="1"> <item value="url1"/> @@ -9,4 +8,7 @@ <item value="url55"/> <item value="url56"/> </string-array> + <boolean name="aps_compliant" value="false"/> + <string name="developer_id" value="dev1"/> + <string name="application_id" value="app1"/> </pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml new file mode 100644 index 000000000000..810078e777fb --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml @@ -0,0 +1,25 @@ +<pbundle_as_map name="app_info"> + <string name="title" value="beervision"/> + <string name="description" value="a beer app"/> + <boolean name="contains_ads" value="true"/> + <boolean name="obey_aps" value="false"/> + <boolean name="ads_fingerprinting" value="false"/> + <boolean name="security_fingerprinting" value="false"/> + <string name="privacy_policy" value="www.example.com"/> + <string-array name="security_endpoints" num="3"> + <item value="url1"/> + <item value="url2"/> + <item value="url3"/> + </string-array> + <string-array name="first_party_endpoints" num="1"> + <item value="url1"/> + </string-array> + <string-array name="service_provider_endpoints" num="2"> + <item value="url55"/> + <item value="url56"/> + </string-array> + <string name="category" value="Food and drink"/> + <string name="email" value="max@maxloh.com"/> + <string name="website" value="www.example.com"/> + <string name="unrecognized" value="www.example.com"/> +</pbundle_as_map> diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-app-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-app-info.xml index 2512ca415d55..2c5cf866547a 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-app-info.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-app-info.xml @@ -2,7 +2,7 @@ <transparency-info> <app-info apsCompliant="false" - privacyPolicy="www.example.com"> + privacyPolicy="www.example.com" developerId="dev1" applicationId="app1"> <first-party-endpoints> <item>url1</item> </first-party-endpoints> diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml index d16caaea320f..29c88d23abad 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml @@ -1,5 +1,16 @@ <transparency-info> + <app-info + apsCompliant="false" + privacyPolicy="www.example.com" developerId="dev1" applicationId="app1"> + <first-party-endpoints> + <item>url1</item> + </first-party-endpoints> + <service-provider-endpoints> + <item>url55</item> + <item>url56</item> + </service-provider-endpoints> + </app-info> <developer-info name="max" email="max@example.com" diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info.xml index c7bdd97a356c..c46cec1da336 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info.xml @@ -1,7 +1,6 @@ <pbundle_as_map name="transparency_info"> <pbundle_as_map name="app_info"> - <boolean name="aps_compliant" value="false"/> <string name="privacy_policy" value="www.example.com"/> <string-array name="first_party_endpoints" num="1"> <item value="url1"/> @@ -10,5 +9,8 @@ <item value="url55"/> <item value="url56"/> </string-array> + <boolean name="aps_compliant" value="false"/> + <string name="developer_id" value="dev1"/> + <string name="application_id" value="app1"/> </pbundle_as_map> </pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml index d7a4e1a959b7..b5e64b925ca5 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml @@ -1,5 +1,18 @@ <pbundle_as_map name="transparency_info"> + <pbundle_as_map name="app_info"> + <string name="privacy_policy" value="www.example.com"/> + <string-array name="first_party_endpoints" num="1"> + <item value="url1"/> + </string-array> + <string-array name="service_provider_endpoints" num="2"> + <item value="url55"/> + <item value="url56"/> + </string-array> + <boolean name="aps_compliant" value="false"/> + <string name="developer_id" value="dev1"/> + <string name="application_id" value="app1"/> + </pbundle_as_map> <pbundle_as_map name="developer_info"> <string name="name" value="max"/> <string name="email" value="max@example.com"/> diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general-v1/od.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general-v1/od.xml new file mode 100644 index 000000000000..e8b0c17ecada --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general-v1/od.xml @@ -0,0 +1,70 @@ +<bundle> + <long name="version" value="1"/> + <pbundle_as_map name="safety_labels"> + <pbundle_as_map name="data_labels"> + <pbundle_as_map name="data_shared"> + <pbundle_as_map name="location"> + <pbundle_as_map name="approx_location"> + <int-array name="purposes" num="1"> + <item value="1"/> + </int-array> + <boolean name="is_sharing_optional" value="false"/> + <boolean name="ephemeral" value="false"/> + </pbundle_as_map> + <pbundle_as_map name="precise_location"> + <int-array name="purposes" num="2"> + <item value="1"/> + <item value="2"/> + </int-array> + <boolean name="is_sharing_optional" value="true"/> + <boolean name="ephemeral" value="true"/> + </pbundle_as_map> + </pbundle_as_map> + </pbundle_as_map> + </pbundle_as_map> + <pbundle_as_map name="security_labels"> + <boolean name="is_data_deletable" value="true"/> + <boolean name="is_data_encrypted" value="false"/> + </pbundle_as_map> + <pbundle_as_map name="third_party_verification"> + <string name="url" value="www.example.com"/> + </pbundle_as_map> + </pbundle_as_map> + <pbundle_as_map name="system_app_safety_label"> + <string name="url" value="www.example.com"/> + </pbundle_as_map> + <pbundle_as_map name="transparency_info"> + <pbundle_as_map name="developer_info"> + <string name="name" value="max"/> + <string name="email" value="max@example.com"/> + <string name="address" value="111 blah lane"/> + <string name="country_region" value="US"/> + <long name="relationship" value="5"/> + <string name="website" value="example.com"/> + <string name="app_developer_registry_id" value="registry_id"/> + </pbundle_as_map> + <pbundle_as_map name="app_info"> + <string name="title" value="beervision"/> + <string name="description" value="a beer app"/> + <boolean name="contains_ads" value="true"/> + <boolean name="obey_aps" value="false"/> + <boolean name="ads_fingerprinting" value="false"/> + <boolean name="security_fingerprinting" value="false"/> + <string name="privacy_policy" value="www.example.com"/> + <string-array name="security_endpoints" num="3"> + <item value="url1"/> + <item value="url2"/> + <item value="url3"/> + </string-array> + <string-array name="first_party_endpoints" num="1"> + <item value="url1"/> + </string-array> + <string-array name="service_provider_endpoints" num="2"> + <item value="url55"/> + <item value="url56"/> + </string-array> + <string name="category" value="Food and drink"/> + <string name="email" value="max@maxloh.com"/> + </pbundle_as_map> + </pbundle_as_map> +</bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/hr.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/hr.xml index 592307937896..f93298286944 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/hr.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/hr.xml @@ -1,20 +1,13 @@ -<app-metadata-bundles version="123"> +<app-metadata-bundles version="2"> <safety-labels> <data-labels> - <data-shared - dataType="location_data_type_approx_location" - isSharingOptional="false" - purposes="app_functionality" /> - <data-shared - dataType="location_data_type_precise_location" - isSharingOptional="true" - purposes="app_functionality|analytics" /> + <data-shared dataType="location_data_type_approx_location" isSharingOptional="false" purposes="app_functionality"/> + <data-shared dataType="location_data_type_precise_location" isSharingOptional="true" purposes="app_functionality|analytics"/> </data-labels> </safety-labels> - <system-app-safety-label declaration="true"> - </system-app-safety-label> + <system-app-safety-label declaration="true"/> <transparency-info> - <app-info apsCompliant="false" privacyPolicy="www.example.com"> + <app-info applicationId="app1" apsCompliant="false" developerId="dev1" privacyPolicy="www.example.com"> <first-party-endpoints> <item>url1</item> </first-party-endpoints> diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/od.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/od.xml index c24087e483b6..c7def7227a34 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/od.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/od.xml @@ -1,5 +1,5 @@ <bundle> - <long name="version" value="123"/> + <long name="version" value="2"/> <pbundle_as_map name="safety_labels"> <pbundle_as_map name="data_labels"> <pbundle_as_map name="data_shared"> @@ -26,7 +26,6 @@ </pbundle_as_map> <pbundle_as_map name="transparency_info"> <pbundle_as_map name="app_info"> - <boolean name="aps_compliant" value="false"/> <string name="privacy_policy" value="www.example.com"/> <string-array name="first_party_endpoints" num="1"> <item value="url1"/> @@ -35,6 +34,9 @@ <item value="url55"/> <item value="url56"/> </string-array> + <boolean name="aps_compliant" value="false"/> + <string name="developer_id" value="dev1"/> + <string name="application_id" value="app1"/> </pbundle_as_map> </pbundle_as_map> </bundle>
\ No newline at end of file |