diff options
680 files changed, 16009 insertions, 10231 deletions
diff --git a/apct-tests/perftests/core/src/android/os/TracePerfTest.java b/apct-tests/perftests/core/src/android/os/TracePerfTest.java index bf7c96a3cb85..d9051240d399 100644 --- a/apct-tests/perftests/core/src/android/os/TracePerfTest.java +++ b/apct-tests/perftests/core/src/android/os/TracePerfTest.java @@ -127,14 +127,38 @@ public class TracePerfTest { public void testInstantPerfettoWithArgs() { PerfettoTrace.instant(FOO_CATEGORY, "testInstantP") .addArg("foo", "bar") - .addFlow(1) + .setFlow(1) .emit(); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { PerfettoTrace.instant(FOO_CATEGORY, "testInstantP") .addArg("foo", "bar") - .addFlow(1) + .setFlow(1) + .emit(); + } + } + + @Test + public void testInstantPerfettoWithProto() { + PerfettoTrace.begin(FOO_CATEGORY, "message_queue_receive") + .beginProto() + .beginNested(2004 /* message_queue */) + .addField(1 /* sending_thread_name */, "foo") + .endNested() + .endProto() + .addTerminatingFlow(5) + .emit(); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + PerfettoTrace.begin(FOO_CATEGORY, "message_queue_receive") + .beginProto() + .beginNested(2004 /* message_queue */) + .addField(1 /* sending_thread_name */, "foo") + .endNested() + .endProto() + .addTerminatingFlow(5) .emit(); } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index ebfda527001d..010006edc995 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -809,7 +809,11 @@ public final class JobServiceContext implements ServiceConnection { if (!verifyCallerLocked(cb)) { return; } - + if (mVerb != VERB_EXECUTING) { + // Any state other than executing means the + // job is in transient or stopped state + return; + } executing = getRunningJobLocked(); } if (executing != null && jobId == executing.getJobId()) { diff --git a/boot/boot-image-profile-extra.txt b/boot/boot-image-profile-extra.txt index ced0d176f174..ce99bfed1ce3 100644 --- a/boot/boot-image-profile-extra.txt +++ b/boot/boot-image-profile-extra.txt @@ -45,11 +45,15 @@ HSPLandroid/os/MessageQueue$OnFileDescriptorEventListener;->* HSPLandroid/os/MessageQueue$StackNodeType;->* HSPLandroid/os/MessageQueue$StateNode;->* HSPLandroid/os/MessageQueue$TimedParkStateNode;->* + +# For now, compile all methods in PerfettoTrace and PerfettoTrackEventExtra. +# Similar to the existing Trace APIs, these new APIs can impact the performance +# of many subsystems including MessageQueue. This also keeps benchmark +# comparisons between both APIs fair. HSPLandroid/os/PerfettoTrace$Category;->* HSPLandroid/os/PerfettoTrace;->* HSPLandroid/os/PerfettoTrackEventExtra;->* -HSPLandroid/os/PerfettoTrackEventExtra$BuilderImpl;->* -HSPLandroid/os/PerfettoTrackEventExtra$NoOpBuilder;->* +HSPLandroid/os/PerfettoTrackEventExtra$Builder;->* HSPLandroid/os/PerfettoTrackEventExtra$ArgBool;->* HSPLandroid/os/PerfettoTrackEventExtra$ArgInt64;->* HSPLandroid/os/PerfettoTrackEventExtra$ArgDouble;->* diff --git a/cmds/idmap2/include/idmap2/Idmap.h b/cmds/idmap2/include/idmap2/Idmap.h index 1f15daf1ba47..335dc97bf964 100644 --- a/cmds/idmap2/include/idmap2/Idmap.h +++ b/cmds/idmap2/include/idmap2/Idmap.h @@ -178,8 +178,8 @@ class IdmapHeader { }; struct IdmapConstraint { - // Constraint type can be TYPE_DISPLAY_ID or TYP_DEVICE_ID, please refer - // to ConstraintType in OverlayConstraint.java + // Constraint type can be android::kOverlayConstraintTypeDisplayId or + // android::kOverlayConstraintTypeDeviceId uint32_t constraint_type; uint32_t constraint_value; diff --git a/core/api/current.txt b/core/api/current.txt index f5dcf2de4c51..4862236a35e3 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -18112,6 +18112,7 @@ package android.graphics.drawable { method public void setThickness(@Px int); method public void setThicknessRatio(@FloatRange(from=0.0f, fromInclusive=false) float); method public void setUseLevel(boolean); + field @FlaggedApi("com.android.graphics.flags.gradient_drawable_shape_rounded_cap") public static final int ARC = 4; // 0x4 field public static final int LINE = 2; // 0x2 field public static final int LINEAR_GRADIENT = 0; // 0x0 field public static final int OVAL = 1; // 0x1 @@ -53801,8 +53802,8 @@ package android.view { method @FlaggedApi("com.android.graphics.hwui.flags.limited_hdr") public void setDesiredHdrHeadroom(@FloatRange(from=0.0f, to=10000.0) float); method public void setSecure(boolean); method public void setSurfaceLifecycle(int); - method public void setZOrderMediaOverlay(boolean); - method public void setZOrderOnTop(boolean); + method @Deprecated @FlaggedApi("android.view.flags.deprecate_surface_view_z_order_apis") public void setZOrderMediaOverlay(boolean); + method @Deprecated @FlaggedApi("android.view.flags.deprecate_surface_view_z_order_apis") public void setZOrderOnTop(boolean); field public static final int SURFACE_LIFECYCLE_DEFAULT = 0; // 0x0 field public static final int SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT = 2; // 0x2 field public static final int SURFACE_LIFECYCLE_FOLLOWS_VISIBILITY = 1; // 0x1 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 89b377314887..4222c7c64672 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1737,6 +1737,7 @@ package android.hardware.display { method @NonNull public int[] getUserDisabledHdrTypes(); method public boolean isMinimalPostProcessingRequested(int); method @RequiresPermission(android.Manifest.permission.ACCESS_SURFACE_FLINGER) public void overrideHdrTypes(int, @NonNull int[]); + method @FlaggedApi("com.android.server.display.feature.flags.delay_implicit_rr_registration_until_rr_accessed") public void resetImplicitRefreshRateCallbackStatus(); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setAreUserDisabledHdrTypesAllowed(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE) public void setGlobalUserPreferredDisplayMode(@NonNull android.view.Display.Mode); method @RequiresPermission(android.Manifest.permission.MODIFY_HDR_CONVERSION_MODE) public void setHdrConversionMode(@NonNull android.hardware.display.HdrConversionMode); diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 6151b8e2ef0a..a12c0674998e 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -23,6 +23,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.PermissionMethod; import android.annotation.PermissionName; +import android.annotation.SpecialUsers.CanBeALL; import android.annotation.UserIdInt; import android.app.ActivityManager.ProcessCapability; import android.app.ActivityManager.RestrictionLevel; @@ -1365,8 +1366,8 @@ public abstract class ActivityManagerInternal { * watchdog reset. * @hide */ - public abstract void killApplicationSync(String pkgName, int appId, int userId, - String reason, int exitInfoReason); + public abstract void killApplicationSync(String pkgName, int appId, + @CanBeALL @UserIdInt int userId, String reason, int exitInfoReason); /** * Queries the offset data for a given method on a process. diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 0519695ff7fe..1a6e9b07067f 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2976,6 +2976,13 @@ class ContextImpl extends Context { if (display != null) { updateDeviceIdIfChanged(display.getDisplayId()); } + updateResourceOverlayConstraints(); + } + + private void updateResourceOverlayConstraints() { + if (mResources != null) { + mResources.getAssets().setOverlayConstraints(getDisplayId(), getDeviceId()); + } } @Override @@ -2988,9 +2995,11 @@ class ContextImpl extends Context { } } - return new ContextImpl(this, mMainThread, mPackageInfo, mParams, + final ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mParams, mAttributionSource.getAttributionTag(), mAttributionSource.getNext(), mSplitName, mToken, mUser, mFlags, mClassLoader, null, deviceId, true); + context.updateResourceOverlayConstraints(); + return context; } @NonNull @@ -3285,6 +3294,7 @@ class ContextImpl extends Context { mDeviceId = updatedDeviceId; mAttributionSource = createAttributionSourceWithDeviceId(mAttributionSource, mDeviceId); notifyOnDeviceChangedListeners(updatedDeviceId); + updateResourceOverlayConstraints(); } } @@ -3700,6 +3710,7 @@ class ContextImpl extends Context { mResourcesManager.setLocaleConfig(lc); } } + updateResourceOverlayConstraints(); } void installSystemApplicationInfo(ApplicationInfo info, ClassLoader classLoader) { diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index 4b1afa517122..01b2953362b5 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -146,6 +146,7 @@ interface IActivityTaskManager { int getFrontActivityScreenCompatMode(); void setFrontActivityScreenCompatMode(int mode); void setFocusedTask(int taskId); + boolean setTaskIsPerceptible(int taskId, boolean isPerceptible); boolean removeTask(int taskId); void removeAllVisibleRecentTasks(); List<ActivityManager.RunningTaskInfo> getTasks(int maxNum, boolean filterOnlyVisibleRecents, diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 5dca1c70a2e6..719e4389d92d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -18,6 +18,7 @@ package android.app; import static android.annotation.Dimension.DP; import static android.app.Flags.evenlyDividedCallStyleActionLayout; +import static android.app.Flags.notificationsRedesignTemplates; import static android.app.admin.DevicePolicyResources.Drawables.Source.NOTIFICATION; import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED; import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; @@ -818,7 +819,8 @@ public class Notification implements Parcelable R.layout.notification_2025_template_expanded_base, R.layout.notification_2025_template_heads_up_base, R.layout.notification_2025_template_header, - R.layout.notification_2025_template_conversation, + R.layout.notification_2025_template_collapsed_conversation, + R.layout.notification_2025_template_expanded_conversation, R.layout.notification_2025_template_collapsed_call, R.layout.notification_2025_template_expanded_call, R.layout.notification_2025_template_collapsed_messaging, @@ -5963,7 +5965,10 @@ public class Notification implements Parcelable || resId == getCompactHeadsUpBaseLayoutResource() || resId == getMessagingCompactHeadsUpLayoutResource() || resId == getCollapsedMessagingLayoutResource() - || resId == getCollapsedMediaLayoutResource()); + || resId == getCollapsedMediaLayoutResource() + || resId == getCollapsedConversationLayoutResource() + || (notificationsRedesignTemplates() + && resId == getCollapsedCallLayoutResource())); RemoteViews contentView = new BuilderRemoteViews(mContext.getApplicationInfo(), resId); resetStandardTemplate(contentView); @@ -6001,8 +6006,7 @@ public class Notification implements Parcelable // Update margins to leave space for the top line (but not for headerless views like // HUNS, which use a different layout that already accounts for that). Templates that // have content that will be displayed under the small icon also use a different margin. - if (Flags.notificationsRedesignTemplates() - && !p.mHeaderless && !p.mSkipTopLineAlignment) { + if (Flags.notificationsRedesignTemplates() && !p.mHeaderless) { int margin = getContentMarginTop(mContext, R.dimen.notification_2025_content_margin_top); contentView.setViewLayoutMargin(R.id.notification_main_column, @@ -7673,12 +7677,18 @@ public class Notification implements Parcelable } } + // Note: In the 2025 redesign, we use two separate layouts for the collapsed and expanded + // version of conversations. See below. private int getConversationLayoutResource() { - if (Flags.notificationsRedesignTemplates()) { - return R.layout.notification_2025_template_conversation; - } else { - return R.layout.notification_template_material_conversation; - } + return R.layout.notification_template_material_conversation; + } + + private int getCollapsedConversationLayoutResource() { + return R.layout.notification_2025_template_collapsed_conversation; + } + + private int getExpandedConversationLayoutResource() { + return R.layout.notification_2025_template_expanded_conversation; } private int getCollapsedCallLayoutResource() { @@ -9462,7 +9472,7 @@ public class Notification implements Parcelable boolean hideRightIcons = viewType != StandardTemplateParams.VIEW_TYPE_NORMAL; boolean isConversationLayout = mConversationType != CONVERSATION_TYPE_LEGACY; boolean isImportantConversation = mConversationType == CONVERSATION_TYPE_IMPORTANT; - boolean isHeaderless = !isConversationLayout && isCollapsed; + boolean isLegacyHeaderless = !isConversationLayout && isCollapsed; //TODO (b/217799515): ensure mConversationTitle always returns the correct // conversationTitle, probably set mConversationTitle = conversationTitle after this @@ -9483,7 +9493,8 @@ public class Notification implements Parcelable } else { isOneToOne = !isGroupConversation(); } - if (isHeaderless && isOneToOne && TextUtils.isEmpty(conversationTitle)) { + if ((isLegacyHeaderless || notificationsRedesignTemplates()) + && isOneToOne && TextUtils.isEmpty(conversationTitle)) { conversationTitle = getOtherPersonName(); } @@ -9493,22 +9504,24 @@ public class Notification implements Parcelable .viewType(viewType) .highlightExpander(isConversationLayout) .hideProgress(true) - .title(isHeaderless ? conversationTitle : null) .text(null) .hideLeftIcon(isOneToOne) - .hideRightIcon(hideRightIcons || isOneToOne) - .headerTextSecondary(isHeaderless ? null : conversationTitle) - .skipTopLineAlignment(true); + .hideRightIcon(hideRightIcons || isOneToOne); + if (notificationsRedesignTemplates()) { + p.title(conversationTitle) + .hideAppName(isCollapsed); + } else { + p.title(isLegacyHeaderless ? conversationTitle : null) + .headerTextSecondary(isLegacyHeaderless ? null : conversationTitle); + } RemoteViews contentView = mBuilder.applyStandardTemplateWithActions( - isConversationLayout - ? mBuilder.getConversationLayoutResource() - : isCollapsed - ? mBuilder.getCollapsedMessagingLayoutResource() - : mBuilder.getExpandedMessagingLayoutResource(), + getMessagingLayoutResource(isConversationLayout, isCollapsed), p, bindResult); - if (isConversationLayout) { + if (isConversationLayout && !notificationsRedesignTemplates()) { + // Redesign note: This view is replaced by the `title`, which is handled normally. mBuilder.setTextViewColorPrimary(contentView, R.id.conversation_text, p); + // Redesign note: This special divider is no longer needed. mBuilder.setTextViewColorSecondary(contentView, R.id.app_name_divider, p); } @@ -9538,7 +9551,18 @@ public class Notification implements Parcelable contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsImportantConversation", isImportantConversation); } - if (isHeaderless) { + if (notificationsRedesignTemplates() && !isCollapsed) { + // Align the title to the app/small icon in the expanded form. In other layouts, + // this margin is added directly to the notification_main_column parent, but for + // messages we don't want the margin to be applied to the actual messaging + // content since it can contain icons that are displayed below the app icon. + Resources res = mBuilder.mContext.getResources(); + int marginStart = res.getDimensionPixelSize( + R.dimen.notification_2025_content_margin_start); + contentView.setViewLayoutMargin(R.id.title, + RemoteViews.MARGIN_START, marginStart, TypedValue.COMPLEX_UNIT_PX); + } + if (isLegacyHeaderless) { // Collapsed legacy messaging style has a 1-line limit. contentView.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1); } @@ -9549,6 +9573,33 @@ public class Notification implements Parcelable return contentView; } + private int getMessagingLayoutResource(boolean isConversationLayout, boolean isCollapsed) { + if (notificationsRedesignTemplates()) { + // Note: We eventually would like to use the same layouts for both conversations and + // regular messaging notifications. + if (isConversationLayout) { + if (isCollapsed) { + return mBuilder.getCollapsedConversationLayoutResource(); + } else { + return mBuilder.getExpandedConversationLayoutResource(); + } + } else { + if (isCollapsed) { + return mBuilder.getCollapsedMessagingLayoutResource(); + } else { + return mBuilder.getExpandedMessagingLayoutResource(); + } + } + + } else { + return isConversationLayout + ? mBuilder.getConversationLayoutResource() + : isCollapsed + ? mBuilder.getCollapsedMessagingLayoutResource() + : mBuilder.getExpandedMessagingLayoutResource(); + } + } + private CharSequence getKey(Person person) { return person == null ? null : person.getKey() == null ? person.getName() : person.getKey(); @@ -10986,6 +11037,7 @@ public class Notification implements Parcelable private RemoteViews makeCallLayout(int viewType) { final boolean isCollapsed = viewType == StandardTemplateParams.VIEW_TYPE_NORMAL; + final boolean isHeadsUp = viewType == StandardTemplateParams.VIEW_TYPE_HEADS_UP; Bundle extras = mBuilder.mN.extras; CharSequence title = mPerson != null ? mPerson.getName() : null; CharSequence text = mBuilder.processLegacyText(extras.getCharSequence(EXTRA_TEXT)); @@ -11001,22 +11053,31 @@ public class Notification implements Parcelable .hideLeftIcon(true) .hideRightIcon(true) .hideAppName(isCollapsed) - .titleViewId(R.id.conversation_text) .title(title) - .text(text) - .summaryText(mBuilder.processLegacyText(mVerificationText)); + .text(text); + if (!notificationsRedesignTemplates()) { + // We're using the normal title in the redesign, not a special text. + p.titleViewId(R.id.conversation_text) + // The verification text is now part of the top line views, so this is no + // longer necessary. + .summaryText(mBuilder.processLegacyText(mVerificationText)); + } mBuilder.mActions = getActionsListWithSystemActions(); final RemoteViews contentView; if (isCollapsed) { contentView = mBuilder.applyStandardTemplate( mBuilder.getCollapsedCallLayoutResource(), p, null /* result */); + } else if (notificationsRedesignTemplates() && isHeadsUp) { + contentView = mBuilder.applyStandardTemplateWithActions( + mBuilder.getCollapsedCallLayoutResource(), p, null /* result */); } else { contentView = mBuilder.applyStandardTemplateWithActions( mBuilder.getExpandedCallLayoutResource(), p, null /* result */); } // Bind some extra conversation-specific header fields. - if (!p.mHideAppName) { + if (!notificationsRedesignTemplates() && !p.mHideAppName) { + // Redesign note: This special divider is no longer needed. mBuilder.setTextViewColorSecondary(contentView, R.id.app_name_divider, p); contentView.setViewVisibility(R.id.app_name_divider, View.VISIBLE); } @@ -14676,7 +14737,6 @@ public class Notification implements Parcelable Icon mPromotedPicture; boolean mCallStyleActions; boolean mAllowTextWithProgress; - boolean mSkipTopLineAlignment; int mTitleViewId; int mTextViewId; @Nullable CharSequence mTitle; @@ -14702,7 +14762,6 @@ public class Notification implements Parcelable mPromotedPicture = null; mCallStyleActions = false; mAllowTextWithProgress = false; - mSkipTopLineAlignment = false; mTitleViewId = R.id.title; mTextViewId = R.id.text; mTitle = null; @@ -14769,11 +14828,6 @@ public class Notification implements Parcelable return this; } - public StandardTemplateParams skipTopLineAlignment(boolean skipTopLineAlignment) { - mSkipTopLineAlignment = skipTopLineAlignment; - return this; - } - final StandardTemplateParams hideSnoozeButton(boolean hideSnoozeButton) { this.mHideSnoozeButton = hideSnoozeButton; return this; diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 08719fc549f8..5359ba44a3d2 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -14200,6 +14200,9 @@ public class DevicePolicyManager { * <li>Manifest.permission.ACTIVITY_RECOGNITION</li> * <li>Manifest.permission.BODY_SENSORS</li> * </ul> + * On devices running {@link android.os.Build.VERSION_CODES#BAKLAVA}, the + * {@link android.health.connect.HealthPermissions} are also included in the + * restricted list. * <p> * A profile owner may not grant these permissions (i.e. call this method with any of the * permissions listed above and {@code grantState} of {@code #PERMISSION_GRANT_STATE_GRANTED}), @@ -17644,9 +17647,17 @@ public class DevicePolicyManager { android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS }) public boolean isFinancedDevice() { - return isDeviceManaged() - && getDeviceOwnerType(getDeviceOwnerComponentOnAnyUser()) - == DEVICE_OWNER_TYPE_FINANCED; + try { + return isDeviceManaged() + && getDeviceOwnerType(getDeviceOwnerComponentOnAnyUser()) + == DEVICE_OWNER_TYPE_FINANCED; + } catch (IllegalStateException e) { + // getDeviceOwnerType() will throw IllegalStateException if the device does not have a + // DO. This can happen under a race condition when the DO is removed after + // isDeviceManaged() (so it still returns true) but before getDeviceOwnerType(). + // In this case, the device should not be considered a financed device. + return false; + } } // TODO(b/315298076): revert ag/25574027 and update the doc diff --git a/core/java/android/app/contextualsearch/flags.aconfig b/core/java/android/app/contextualsearch/flags.aconfig index bc1f7cea7fce..1de034b23a7a 100644 --- a/core/java/android/app/contextualsearch/flags.aconfig +++ b/core/java/android/app/contextualsearch/flags.aconfig @@ -24,7 +24,7 @@ flag { } flag { - name: "contextual_search_window_layer" + name: "contextual_search_prevent_self_capture" namespace: "sysui_integrations" description: "Identify live contextual search UI to exclude from contextual search screenshot." bug: "390176823" diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig index 94de03877fd7..709418393479 100644 --- a/core/java/android/app/supervision/flags.aconfig +++ b/core/java/android/app/supervision/flags.aconfig @@ -72,3 +72,11 @@ flag { description: "Flag that enables system APIs in Supervision Manager" bug: "382034839" } + +flag { + name: "enable_web_content_filters_screen" + is_exported: true + namespace: "supervision" + description: "Flag that enables the web content filters screen with Supervision settings entry point" + bug: "395134536" +} diff --git a/core/java/android/appwidget/OWNERS b/core/java/android/appwidget/OWNERS index 0e85d5bd7a27..1dc4cbb701fa 100644 --- a/core/java/android/appwidget/OWNERS +++ b/core/java/android/appwidget/OWNERS @@ -3,5 +3,4 @@ sihua@google.com pinyaoting@google.com suprabh@google.com sunnygoyal@google.com -zakcohen@google.com shamalip@google.com diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index ba1473cf5ed7..67ade79e1b94 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -135,3 +135,11 @@ flag { description: "Show virtual devices in Settings" bug: "338974320" } + +flag { + name: "migrate_viewconfiguration_constants_to_resources" + namespace: "virtual_devices" + description: "Use resources instead of constants in ViewConfiguration" + is_fixed_read_only: true + bug: "370928384" +} diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index efcaa0ea6f07..a753cbf956c6 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -23,6 +23,8 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SpecialUsers.CanBeALL; +import android.annotation.SpecialUsers.CanBeCURRENT; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; @@ -2709,7 +2711,7 @@ public abstract class ContentResolver implements ContentInterface { public final void registerContentObserverAsUser(@NonNull Uri uri, boolean notifyForDescendants, @NonNull ContentObserver observer, - @NonNull UserHandle userHandle) { + @NonNull @CanBeALL @CanBeCURRENT UserHandle userHandle) { Objects.requireNonNull(uri, "uri"); Objects.requireNonNull(observer, "observer"); Objects.requireNonNull(userHandle, "userHandle"); @@ -2723,7 +2725,7 @@ public abstract class ContentResolver implements ContentInterface { /** @hide - designated user version */ @UnsupportedAppUsage public final void registerContentObserver(Uri uri, boolean notifyForDescendants, - ContentObserver observer, @UserIdInt int userHandle) { + ContentObserver observer, @CanBeALL @CanBeCURRENT @UserIdInt int userHandle) { try { getContentService().registerContentObserver(uri, notifyForDescendants, observer.getContentObserver(), userHandle, mTargetSdkVersion); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index f91b2474fdac..9e91f5944504 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -2038,7 +2038,7 @@ public abstract class PackageManager { public static final int INSTALL_SCENARIO_DEFAULT = 0; /** - * Installation scenario providing the fastest “install button to launch" experience possible. + * Installation scenario providing the fastest "install button to launch" experience possible. */ public static final int INSTALL_SCENARIO_FAST = 1; @@ -3585,7 +3585,7 @@ public abstract class PackageManager { /** * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: The device is - * compatible with Android’s security model. + * compatible with Android's security model. * * <p>See sections 2 and 9 in the * <a href="https://source.android.com/compatibility/android-cdd">Android CDD</a> for more diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index e4b8c90d381d..255a08cf170f 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -391,3 +391,12 @@ flag { bug: "319137634" is_fixed_read_only: true } + +flag { + name: "always_load_past_certs_v4" + is_exported: true + namespace: "package_manager_service" + description: "Always read the corresponding v3/3.1 signature block for the current v4 to get the past rotated certificates, even when not verifying integrity." + bug: "378539511" + is_fixed_read_only: true +} diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index 7cd2d31ac974..bcb50881d327 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -148,8 +148,8 @@ public final class AssetManager implements AutoCloseable { * @hide */ public static class Builder { - private ArrayList<ApkAssets> mUserApkAssets = new ArrayList<>(); - private ArrayList<ResourcesLoader> mLoaders = new ArrayList<>(); + private final ArrayList<ApkAssets> mUserApkAssets = new ArrayList<>(); + private final ArrayList<ResourcesLoader> mLoaders = new ArrayList<>(); private boolean mNoInit = false; @@ -1625,6 +1625,23 @@ public final class AssetManager implements AutoCloseable { } /** + * Passes the display id and device id to AssetManager, to filter out overlays based on + * any {@link android.content.om.OverlayConstraint}. + * + * @hide + */ + public void setOverlayConstraints(int displayId, int deviceId) { + if (!Flags.rroConstraints()) { + return; + } + + synchronized (this) { + ensureValidLocked(); + nativeSetOverlayConstraints(mObject, displayId, deviceId); + } + } + + /** * @hide */ @UnsupportedAppUsage @@ -1717,6 +1734,7 @@ public final class AssetManager implements AutoCloseable { int screenWidth, int screenHeight, int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, int screenLayout, int uiMode, int colorMode, int grammaticalGender, int majorVersion, boolean forceRefresh); + private static native void nativeSetOverlayConstraints(long ptr, int displayId, int deviceId); private static native @NonNull SparseArray<String> nativeGetAssignedPackageIdentifiers( long ptr, boolean includeOverlays, boolean includeLoaders); diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index ef200c328d63..2e0999410483 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -2358,8 +2358,13 @@ public final class Configuration implements Parcelable, Comparable<Configuration * @param locales The locale list. If null, an empty LocaleList will be assigned. */ public void setLocales(@Nullable LocaleList locales) { + LocaleList oldList = mLocaleList; mLocaleList = locales == null ? LocaleList.getEmptyLocaleList() : locales; locale = mLocaleList.get(0); + if (!mLocaleList.equals(oldList)) { + Slog.v(TAG, "Updating configuration, locales updated from " + oldList + + " to " + mLocaleList); + } setLayoutDirection(locale); } diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index ddad54d94cc6..8c76fd70afd9 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -491,6 +491,9 @@ public class ResourcesImpl { } defaultLocale = adjustLanguageTag(lc.getDefaultLocale().toLanguageTag()); + Slog.v(TAG, "Updating configuration, with default locale " + + defaultLocale + " and selected locales " + + Arrays.toString(selectedLocales)); } else { String[] availableLocales; // The LocaleList has changed. We must query the AssetManager's @@ -526,6 +529,7 @@ public class ResourcesImpl { for (int i = 0; i < locales.size(); i++) { selectedLocales[i] = adjustLanguageTag(locales.get(i).toLanguageTag()); } + defaultLocale = adjustLanguageTag(lc.getDefaultLocale().toLanguageTag()); } else { selectedLocales = new String[]{ adjustLanguageTag(locales.get(0).toLanguageTag())}; diff --git a/core/java/android/hardware/contexthub/HubEndpoint.java b/core/java/android/hardware/contexthub/HubEndpoint.java index b7edef619a67..47ba51d98323 100644 --- a/core/java/android/hardware/contexthub/HubEndpoint.java +++ b/core/java/android/hardware/contexthub/HubEndpoint.java @@ -126,7 +126,16 @@ public class HubEndpoint { if (sessionExists) { Log.w( TAG, - "onSessionOpenComplete: session already exists, id=" + sessionId); + "onSessionOpenRequest: session already exists, id=" + sessionId); + } + + if (mLifecycleCallback == null) { + Log.w( + TAG, + "onSessionOpenRequest: " + + "failed to open session, no lifecycle callback attached", + new Exception()); + rejectSession(sessionId); } if (!sessionExists && mLifecycleCallback != null) { diff --git a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl index eb1255c06094..5e6bb2d434d5 100644 --- a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl +++ b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl @@ -99,5 +99,5 @@ interface IContextHubEndpoint { * Invoked when a callback from IContextHubEndpointCallback finishes. */ @EnforcePermission("ACCESS_CONTEXT_HUB") - void onCallbackFinished(); + oneway void onCallbackFinished(); } diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 7850e377ec4d..92a56fc575e3 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -851,6 +851,12 @@ public final class DisplayManager { * Registers a display listener to receive notifications about when * displays are added, removed or changed. * + * Because of the high frequency at which the refresh rate can change, clients will be + * registered for refresh rate change callbacks only when they request for refresh rate + * data({@link Display#getRefreshRate()}. Or alternately, you can consider using + * {@link #registerDisplayListener(Executor, long, DisplayListener)} and explicitly subscribe to + * {@link #EVENT_TYPE_DISPLAY_REFRESH_RATE} event + * * We encourage to use {@link #registerDisplayListener(Executor, long, DisplayListener)} * instead to subscribe for explicit events of interest * @@ -863,8 +869,8 @@ public final class DisplayManager { public void registerDisplayListener(DisplayListener listener, Handler handler) { registerDisplayListener(listener, handler, EVENT_TYPE_DISPLAY_ADDED | EVENT_TYPE_DISPLAY_CHANGED - | EVENT_TYPE_DISPLAY_REFRESH_RATE - | EVENT_TYPE_DISPLAY_REMOVED); + | EVENT_TYPE_DISPLAY_REMOVED, 0, + ActivityThread.currentPackageName(), /* isEventFilterExplicit */ false); } /** @@ -882,9 +888,8 @@ public final class DisplayManager { */ public void registerDisplayListener(@NonNull DisplayListener listener, @Nullable Handler handler, @EventType long eventFilter) { - mGlobal.registerDisplayListener(listener, handler, - mGlobal.mapFiltersToInternalEventFlag(eventFilter, 0), - ActivityThread.currentPackageName()); + registerDisplayListener(listener, handler, eventFilter, 0, + ActivityThread.currentPackageName(), /* isEventFilterExplicit */ true); } /** @@ -901,9 +906,8 @@ public final class DisplayManager { @FlaggedApi(FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS) public void registerDisplayListener(@NonNull Executor executor, @EventType long eventFilter, @NonNull DisplayListener listener) { - mGlobal.registerDisplayListener(listener, executor, - mGlobal.mapFiltersToInternalEventFlag(eventFilter, 0), - ActivityThread.currentPackageName()); + registerDisplayListener(listener, executor, eventFilter, 0, + ActivityThread.currentPackageName(), /* isEventFilterExplicit */ true); } /** @@ -924,9 +928,39 @@ public final class DisplayManager { public void registerDisplayListener(@NonNull DisplayListener listener, @Nullable Handler handler, @EventType long eventFilter, @PrivateEventType long privateEventFilter) { + registerDisplayListener(listener, handler, eventFilter, privateEventFilter, + ActivityThread.currentPackageName(), /* isEventFilterExplicit */ true); + } + + /** + * Registers a display listener to receive notifications about given display event types. + * + * @param listener The listener to register. + * @param handler The handler on which the listener should be invoked, or null + * if the listener should be invoked on the calling thread's looper. + * @param eventFilter A bitmask of the event types for which this listener is subscribed. + * @param privateEventFilter A bitmask of the private event types for which this listener + * is subscribed. + * @param isEventFilterExplicit Indicates if the client explicitly supplied the display events + * to be subscribed to. + * + */ + private void registerDisplayListener(@NonNull DisplayListener listener, + @Nullable Handler handler, @EventType long eventFilter, + @PrivateEventType long privateEventFilter, String packageName, + boolean isEventFilterExplicit) { mGlobal.registerDisplayListener(listener, handler, mGlobal.mapFiltersToInternalEventFlag(eventFilter, privateEventFilter), - ActivityThread.currentPackageName()); + packageName, /* isEventFilterExplicit */ isEventFilterExplicit); + } + + private void registerDisplayListener(@NonNull DisplayListener listener, + Executor executor, @EventType long eventFilter, + @PrivateEventType long privateEventFilter, String packageName, + boolean isEventFilterExplicit) { + mGlobal.registerDisplayListener(listener, executor, + mGlobal.mapFiltersToInternalEventFlag(eventFilter, privateEventFilter), + packageName, /* isEventFilterExplicit */ isEventFilterExplicit); } /** @@ -1146,6 +1180,28 @@ public final class DisplayManager { } /** + * Resets the behavior that automatically registers clients for refresh rate change callbacks + * when they register via {@link #registerDisplayListener(DisplayListener, Handler)} + * + * <p>By default, clients are not registered for refresh rate change callbacks via + * {@link #registerDisplayListener(DisplayListener, Handler)}. However, calling + * {@link Display#getRefreshRate()} triggers automatic registration for existing and future + * {@link DisplayListener} instances. This method reverts this behavior, preventing new + * clients from being automatically registered for refresh rate change callbacks. Note that the + * existing ones will continue to stay registered + * + * <p>In essence, this method returns the system to its initial state, where explicit calls to + * {{@link Display#getRefreshRate()} are required to receive refresh rate change notifications. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_DELAY_IMPLICIT_RR_REGISTRATION_UNTIL_RR_ACCESSED) + @TestApi + public void resetImplicitRefreshRateCallbackStatus() { + mGlobal.resetImplicitRefreshRateCallbackStatus(); + } + + /** * Overrides HDR modes for a display device. * * @hide diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index a7d610e54e2c..c4af87116eed 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -187,6 +187,9 @@ public final class DisplayManagerGlobal { private final Binder mToken = new Binder(); + // Guarded by mLock + private boolean mShouldImplicitlyRegisterRrChanges = false; + @VisibleForTesting public DisplayManagerGlobal(IDisplayManager dm) { mDm = dm; @@ -390,27 +393,49 @@ public final class DisplayManagerGlobal { * the handler for the main thread. * If that is still null, a runtime exception will be thrown. * @param packageName of the calling package. + * @param isEventFilterExplicit Indicates if the client explicitly supplied the display events + * to be subscribed to. */ public void registerDisplayListener(@NonNull DisplayListener listener, @Nullable Handler handler, @InternalEventFlag long internalEventFlagsMask, - String packageName) { + String packageName, boolean isEventFilterExplicit) { Looper looper = getLooperForHandler(handler); Handler springBoard = new Handler(looper); registerDisplayListener(listener, new HandlerExecutor(springBoard), internalEventFlagsMask, - packageName); + packageName, isEventFilterExplicit); } /** * Register a listener for display-related changes. * * @param listener The listener that will be called when display changes occur. + * @param handler Handler for the thread that will be receiving the callbacks. May be null. + * If null, listener will use the handler for the current thread, and if still null, + * the handler for the main thread. + * If that is still null, a runtime exception will be thrown. + * @param internalEventFlagsMask Mask of events to be listened to. + * @param packageName of the calling package. + */ + public void registerDisplayListener(@NonNull DisplayListener listener, + @Nullable Handler handler, @InternalEventFlag long internalEventFlagsMask, + String packageName) { + registerDisplayListener(listener, handler, internalEventFlagsMask, packageName, true); + } + + + /** + * Register a listener for display-related changes. + * + * @param listener The listener that will be called when display changes occur. * @param executor Executor for the thread that will be receiving the callbacks. Cannot be null. * @param internalEventFlagsMask Mask of events to be listened to. * @param packageName of the calling package. + * @param isEventFilterExplicit Indicates if the explicit events to be subscribed to + * were supplied or not */ public void registerDisplayListener(@NonNull DisplayListener listener, @NonNull Executor executor, @InternalEventFlag long internalEventFlagsMask, - String packageName) { + String packageName, boolean isEventFilterExplicit) { if (listener == null) { throw new IllegalArgumentException("listener must not be null"); } @@ -429,7 +454,7 @@ public final class DisplayManagerGlobal { int index = findDisplayListenerLocked(listener); if (index < 0) { mDisplayListeners.add(new DisplayListenerDelegate(listener, executor, - internalEventFlagsMask, packageName)); + internalEventFlagsMask, packageName, isEventFilterExplicit)); registerCallbackIfNeededLocked(); } else { mDisplayListeners.get(index).setEventsMask(internalEventFlagsMask); @@ -439,6 +464,22 @@ public final class DisplayManagerGlobal { } } + + /** + * Registers all the clients to INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE events if qualified + */ + public void registerForRefreshRateChanges() { + if (!Flags.delayImplicitRrRegistrationUntilRrAccessed()) { + return; + } + synchronized (mLock) { + if (!mShouldImplicitlyRegisterRrChanges) { + mShouldImplicitlyRegisterRrChanges = true; + updateCallbackIfNeededLocked(); + } + } + } + public void unregisterDisplayListener(DisplayListener listener) { if (listener == null) { throw new IllegalArgumentException("listener must not be null"); @@ -521,8 +562,14 @@ public final class DisplayManagerGlobal { long mask = 0; final int numListeners = mDisplayListeners.size(); for (int i = 0; i < numListeners; i++) { - mask |= mDisplayListeners.get(i).mInternalEventFlagsMask; + DisplayListenerDelegate displayListenerDelegate = mDisplayListeners.get(i); + if (!Flags.delayImplicitRrRegistrationUntilRrAccessed() + || mShouldImplicitlyRegisterRrChanges) { + displayListenerDelegate.implicitlyRegisterForRRChanges(); + } + mask |= displayListenerDelegate.mInternalEventFlagsMask; } + if (mDispatchNativeCallbacks) { mask |= INTERNAL_EVENT_FLAG_DISPLAY_ADDED | INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED @@ -802,6 +849,18 @@ public final class DisplayManagerGlobal { } /** + * Resets the implicit registration of refresh rate change callbacks + * + */ + public void resetImplicitRefreshRateCallbackStatus() { + if (Flags.delayImplicitRrRegistrationUntilRrAccessed()) { + synchronized (mLock) { + mShouldImplicitlyRegisterRrChanges = false; + } + } + } + + /** * Overrides HDR modes for a display device. * */ @@ -1439,21 +1498,27 @@ public final class DisplayManagerGlobal { } } - private static final class DisplayListenerDelegate { + @VisibleForTesting + static final class DisplayListenerDelegate { public final DisplayListener mListener; public volatile long mInternalEventFlagsMask; + // Indicates if the client explicitly supplied the display events to be subscribed to. + private final boolean mIsEventFilterExplicit; + private final DisplayInfo mDisplayInfo = new DisplayInfo(); private final Executor mExecutor; private AtomicLong mGenerationId = new AtomicLong(1); private final String mPackageName; DisplayListenerDelegate(DisplayListener listener, @NonNull Executor executor, - @InternalEventFlag long internalEventFlag, String packageName) { + @InternalEventFlag long internalEventFlag, String packageName, + boolean isEventFilterExplicit) { mExecutor = executor; mListener = listener; mInternalEventFlagsMask = internalEventFlag; mPackageName = packageName; + mIsEventFilterExplicit = isEventFilterExplicit; } void sendDisplayEvent(int displayId, @DisplayEvent int event, @Nullable DisplayInfo info, @@ -1470,6 +1535,11 @@ public final class DisplayManagerGlobal { }); } + @VisibleForTesting + boolean isEventFilterExplicit() { + return mIsEventFilterExplicit; + } + void clearEvents() { mGenerationId.incrementAndGet(); } @@ -1478,6 +1548,17 @@ public final class DisplayManagerGlobal { mInternalEventFlagsMask = newInternalEventFlagsMask; } + private void implicitlyRegisterForRRChanges() { + // For backward compatibility, if the user didn't supply the explicit events while + // subscribing, register them to refresh rate change events if they subscribed to + // display changed events + if ((mInternalEventFlagsMask & INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED) != 0 + && !mIsEventFilterExplicit) { + setEventsMask(mInternalEventFlagsMask + | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE); + } + } + private void handleDisplayEventInner(int displayId, @DisplayEvent int event, @Nullable DisplayInfo info, boolean forceUpdate) { if (extraLogging()) { @@ -1677,6 +1758,9 @@ public final class DisplayManagerGlobal { public void registerNativeChoreographerForRefreshRateCallbacks() { synchronized (mLock) { mDispatchNativeCallbacks = true; + if (Flags.delayImplicitRrRegistrationUntilRrAccessed()) { + mShouldImplicitlyRegisterRrChanges = true; + } registerCallbackIfNeededLocked(); updateCallbackIfNeededLocked(); DisplayInfo display = getDisplayInfoLocked(Display.DEFAULT_DISPLAY); @@ -1806,4 +1890,9 @@ public final class DisplayManagerGlobal { return baseEventMask; } + + @VisibleForTesting + CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() { + return mDisplayListeners; + } } diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl index bb5491d98cf9..71870344eda3 100644 --- a/core/java/android/hardware/location/IContextHubService.aidl +++ b/core/java/android/hardware/location/IContextHubService.aidl @@ -153,5 +153,5 @@ interface IContextHubService { // Called when a discovery callback is finished executing @EnforcePermission("ACCESS_CONTEXT_HUB") - void onDiscoveryCallbackFinished(); + oneway void onDiscoveryCallbackFinished(); } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index a528ba4b16bf..7b47efd47008 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -990,6 +990,8 @@ public class InputMethodService extends AbstractInputMethodService { } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (android.view.inputmethod.Flags.refactorInsetsController()) { + // After the IME window was hidden, we can remove its surface + scheduleImeSurfaceRemoval(); // The hide request first finishes the animation and then proceeds to the server // side, finally reaching here, marking this the end state. ImeTracker.forLogging().onHidden(statsToken); diff --git a/core/java/android/os/BundleMerger.java b/core/java/android/os/BundleMerger.java index dc243a5e9c08..fd5a18c22a37 100644 --- a/core/java/android/os/BundleMerger.java +++ b/core/java/android/os/BundleMerger.java @@ -119,11 +119,23 @@ public class BundleMerger implements Parcelable { public static final int STRATEGY_ARRAY_APPEND = 50; /** + * Merge strategy that combines two conflicting array values by creating a new array + * containing all unique elements from both arrays. + */ + public static final int STRATEGY_ARRAY_UNION = 55; + + /** * Merge strategy that combines two conflicting {@link ArrayList} values by * appending the last {@link ArrayList} after the first {@link ArrayList}. */ public static final int STRATEGY_ARRAY_LIST_APPEND = 60; + /** + * Merge strategy that combines two conflicting {@link String} values by + * appending the last {@link String} after the first {@link String}. + */ + public static final int STRATEGY_STRING_APPEND = 70; + @IntDef(flag = false, prefix = { "STRATEGY_" }, value = { STRATEGY_REJECT, STRATEGY_FIRST, @@ -136,7 +148,9 @@ public class BundleMerger implements Parcelable { STRATEGY_BOOLEAN_AND, STRATEGY_BOOLEAN_OR, STRATEGY_ARRAY_APPEND, + STRATEGY_ARRAY_UNION, STRATEGY_ARRAY_LIST_APPEND, + STRATEGY_STRING_APPEND, }) @Retention(RetentionPolicy.SOURCE) public @interface Strategy {} @@ -298,8 +312,12 @@ public class BundleMerger implements Parcelable { return booleanOr(first, last); case STRATEGY_ARRAY_APPEND: return arrayAppend(first, last); + case STRATEGY_ARRAY_UNION: + return arrayUnion(first, last); case STRATEGY_ARRAY_LIST_APPEND: return arrayListAppend(first, last); + case STRATEGY_STRING_APPEND: + return stringAppend(first, last); default: throw new UnsupportedOperationException(); } @@ -361,6 +379,29 @@ public class BundleMerger implements Parcelable { return res; } + private static @NonNull Object arrayUnion(@NonNull Object first, @NonNull Object last) { + if (!first.getClass().isArray()) { + throw new IllegalArgumentException("Unable to union " + first.getClass()); + } + final int firstLength = Array.getLength(first); + final int lastLength = Array.getLength(last); + final ArrayList<Object> list = new ArrayList<>(firstLength + lastLength); + final ArraySet<Object> set = new ArraySet<>(); + for (int i = 0; i < firstLength; i++) { + set.add(Array.get(first, i)); + } + for (int i = 0; i < lastLength; i++) { + set.add(Array.get(last, i)); + } + final Class<?> clazz = first.getClass().getComponentType(); + final int setSize = set.size(); + final Object res = Array.newInstance(clazz, setSize); + for (int i = 0; i < setSize; i++) { + Array.set(res, i, set.valueAt(i)); + } + return res; + } + @SuppressWarnings("unchecked") private static @NonNull Object arrayListAppend(@NonNull Object first, @NonNull Object last) { if (!(first instanceof ArrayList)) { @@ -374,6 +415,13 @@ public class BundleMerger implements Parcelable { return res; } + private static @NonNull Object stringAppend(@NonNull Object first, @NonNull Object last) { + if (!(first instanceof String)) { + throw new IllegalArgumentException("Unable to append " + first.getClass()); + } + return ((String) first) + ((String) last); + } + public static final @android.annotation.NonNull Parcelable.Creator<BundleMerger> CREATOR = new Parcelable.Creator<BundleMerger>() { @Override diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java index 5886fa45153e..3c03bb5626c8 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -233,9 +233,13 @@ public final class MessageQueue { traceMessageCount(); PerfettoTrace.instant(PerfettoTrace.MQ_CATEGORY, "message_queue_send") .addFlow(msg.mEventId.get()) - .addArg("receiving_thread", mThread.getName()) - .addArg("delay", when - SystemClock.uptimeMillis()) - .addArg("what", msg.what) + .beginProto() + .beginNested(2004 /* message_queue */) + .addField(2 /* receiving_thread_name */, mThread.getName()) + .addField(3 /* message_code */, msg.what) + .addField(4 /* message_delay_ms */, when - SystemClock.uptimeMillis()) + .endNested() + .endProto() .emit(); } diff --git a/core/java/android/os/LockedMessageQueue/MessageQueue.java b/core/java/android/os/LockedMessageQueue/MessageQueue.java deleted file mode 100644 index 2401f3d11bcf..000000000000 --- a/core/java/android/os/LockedMessageQueue/MessageQueue.java +++ /dev/null @@ -1,1364 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.os; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.TestApi; -import android.compat.annotation.UnsupportedAppUsage; -import android.ravenwood.annotation.RavenwoodKeepWholeClass; -import android.ravenwood.annotation.RavenwoodRedirect; -import android.ravenwood.annotation.RavenwoodRedirectionClass; -import android.util.Log; -import android.util.Printer; -import android.util.SparseArray; -import android.util.proto.ProtoOutputStream; - -import com.android.internal.annotations.GuardedBy; - -import dalvik.annotation.optimization.NeverCompile; - -import java.io.FileDescriptor; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Low-level class holding the list of messages to be dispatched by a - * {@link Looper}. Messages are not added directly to a MessageQueue, - * but rather through {@link Handler} objects associated with the Looper. - * - * <p>You can retrieve the MessageQueue for the current thread with - * {@link Looper#myQueue() Looper.myQueue()}. - */ -@RavenwoodKeepWholeClass -@RavenwoodRedirectionClass("MessageQueue_ravenwood") -public final class MessageQueue { - private static final String TAG = "LockedMessageQueue"; - private static final boolean DEBUG = false; - private static final boolean TRACE = false; - - static final class MessageHeap { - static final int MESSAGE_HEAP_INITIAL_SIZE = 16; - - Message[] mHeap = new Message[MESSAGE_HEAP_INITIAL_SIZE]; - int mNumElements = 0; - - static int parentNodeIdx(int i) { - return (i - 1) >>> 1; - } - - Message getParentNode(int i) { - return mHeap[(i - 1) >>> 1]; - } - - static int rightNodeIdx(int i) { - return 2 * i + 2; - } - - Message getRightNode(int i) { - return mHeap[2 * i + 2]; - } - - static int leftNodeIdx(int i) { - return 2 * i + 1; - } - - Message getLeftNode(int i) { - return mHeap[2 * i + 1]; - } - - int size() { - return mHeap.length; - } - - int numElements() { - return mNumElements; - } - - boolean isEmpty() { - return mNumElements == 0; - } - - Message getMessageAt(int index) { - return mHeap[index]; - } - - /* - * Returns: - * 0 if x==y. - * A value less than 0 if x<y. - * A value greater than 0 if x>y. - */ - int compareMessage(Message x, Message y) { - int compared = Long.compare(x.when, y.when); - if (compared == 0) { - compared = Long.compare(x.mInsertSeq, y.mInsertSeq); - } - return compared; - } - - int compareMessageByIdx(int x, int y) { - return compareMessage(mHeap[x], mHeap[y]); - } - - void swap(int x, int y) { - Message tmp = mHeap[x]; - mHeap[x] = mHeap[y]; - mHeap[y] = tmp; - } - - void siftDown(int i) { - int smallest = i; - int r, l; - - while (true) { - r = rightNodeIdx(i); - l = leftNodeIdx(i); - - if (r < mNumElements && compareMessageByIdx(r, smallest) < 0) { - smallest = r; - } - - if (l < mNumElements && compareMessageByIdx(l, smallest) < 0) { - smallest = l; - } - - if (smallest != i) { - swap(i, smallest); - i = smallest; - continue; - } - break; - } - } - - boolean siftUp(int i) { - boolean swapped = false; - while (i != 0 && compareMessage(mHeap[i], getParentNode(i)) < 0) { - int p = parentNodeIdx(i); - - swap(i, p); - swapped = true; - i = p; - } - - return swapped; - } - - void maybeGrowHeap() { - if (mNumElements == mHeap.length) { - /* Grow by 1.5x */ - int newSize = mHeap.length + (mHeap.length >>> 1); - Message[] newHeap; - if (DEBUG) { - Log.v(TAG, "maybeGrowHeap mNumElements " + mNumElements + " mHeap.length " - + mHeap.length + " newSize " + newSize); - } - - newHeap = Arrays.copyOf(mHeap, newSize); - mHeap = newHeap; - } - } - - void add(Message m) { - int i; - - maybeGrowHeap(); - - i = mNumElements; - mNumElements++; - mHeap[i] = m; - - siftUp(i); - } - - void maybeShrinkHeap() { - /* Shrink by 2x */ - int newSize = mHeap.length >>> 1; - - if (newSize >= MESSAGE_HEAP_INITIAL_SIZE - && mNumElements <= newSize) { - Message[] newHeap; - - if (DEBUG) { - Log.v(TAG, "maybeShrinkHeap mNumElements " + mNumElements + " mHeap.length " - + mHeap.length + " newSize " + newSize); - } - - newHeap = Arrays.copyOf(mHeap, newSize); - mHeap = newHeap; - } - } - - Message poll() { - if (mNumElements > 0) { - Message ret = mHeap[0]; - mNumElements--; - mHeap[0] = mHeap[mNumElements]; - mHeap[mNumElements] = null; - - siftDown(0); - - maybeShrinkHeap(); - return ret; - } - return null; - } - - Message peek() { - if (mNumElements > 0) { - return mHeap[0]; - } - return null; - } - - private void remove(int i) throws IllegalArgumentException { - if (i > mNumElements || mNumElements == 0) { - throw new IllegalArgumentException("Index " + i + " out of bounds: " - + mNumElements); - } else if (i == (mNumElements - 1)) { - mHeap[i] = null; - mNumElements--; - } else { - mNumElements--; - mHeap[i] = mHeap[mNumElements]; - mHeap[mNumElements] = null; - if (!siftUp(i)) { - siftDown(i); - } - } - /* Don't shink here, let the caller do this once it has removed all matching items. */ - } - - void removeAll() { - Message m; - for (int i = 0; i < mNumElements; i++) { - m = mHeap[i]; - mHeap[i] = null; - m.recycleUnchecked(); - } - mNumElements = 0; - maybeShrinkHeap(); - } - - abstract static class MessageHeapCompare { - public abstract boolean compareMessage(Message m, Handler h, int what, Object object, - Runnable r, long when); - } - - boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when, - MessageHeapCompare compare, boolean removeMatches) { - boolean found = false; - /* - * Walk the heap backwards so we don't have to re-visit an array element due to - * sifting - */ - for (int i = mNumElements - 1; i >= 0; i--) { - if (compare.compareMessage(mHeap[i], h, what, object, r, when)) { - found = true; - if (removeMatches) { - Message m = mHeap[i]; - try { - remove(i); - } catch (IllegalArgumentException e) { - Log.wtf(TAG, "Index out of bounds during remove " + e); - } - m.recycleUnchecked(); - continue; - } - break; - } - } - if (found && removeMatches) { - maybeShrinkHeap(); - } - return found; - } - - /* - * Keep this for manual debugging. It's easier to pepper the code with this function - * than MessageQueue.dump() - */ - @NeverCompile - void print() { - Log.v(TAG, "heap num elem: " + mNumElements + " mHeap.length " + mHeap.length); - for (int i = 0; i < mNumElements; i++) { - Log.v(TAG, "[" + i + "]\t" + mHeap[i] + " seq: " + mHeap[i].mInsertSeq + " async: " - + mHeap[i].isAsynchronous()); - } - } - - boolean verify(int root) { - int r = rightNodeIdx(root); - int l = leftNodeIdx(root); - - if (l >= mNumElements && r >= mNumElements) { - return true; - } - - if (l < mNumElements && compareMessageByIdx(l, root) < 0) { - Log.wtf(TAG, "Verify failure: root idx/when: " + root + "/" + mHeap[root].when - + " left node idx/when: " + l + "/" + mHeap[l].when); - return false; - } - - if (r < mNumElements && compareMessageByIdx(r, root) < 0) { - Log.wtf(TAG, "Verify failure: root idx/when: " + root + "/" + mHeap[root].when - + " right node idx/when: " + r + "/" + mHeap[r].when); - return false; - } - - if (!verify(r) || !verify(l)) { - return false; - } - return true; - } - - boolean checkDanglingReferences(String where) { - /* First, let's make sure we didn't leave any dangling references */ - for (int i = mNumElements; i < mHeap.length; i++) { - if (mHeap[i] != null) { - Log.wtf(TAG, "[" + where - + "] Verify failure: dangling reference found at index " - + i + ": " + mHeap[i] + " Async " + mHeap[i].isAsynchronous() - + " mNumElements " + mNumElements + " mHeap.length " + mHeap.length); - return false; - } - } - return true; - } - - boolean verify() { - if (!checkDanglingReferences(TAG)) { - return false; - } - return verify(0); - } - } - - // True if the message queue can be quit. - @UnsupportedAppUsage - private final boolean mQuitAllowed; - - @UnsupportedAppUsage - @SuppressWarnings("unused") - private long mPtr; // used by native code - - private final MessageHeap mPriorityQueue = new MessageHeap(); - private final MessageHeap mAsyncPriorityQueue = new MessageHeap(); - - /* - * This helps us ensure that messages with the same timestamp are inserted in FIFO order. - * Increments on each insert, starting at 0. MessaeHeap.compareMessage() will compare sequences - * when delivery timestamps are identical. - */ - private long mNextInsertSeq; - - /* - * The exception to the FIFO order rule is sendMessageAtFrontOfQueue(). - * Those messages must be in LIFO order. - * Decrements on each front of queue insert. - */ - private long mNextFrontInsertSeq = -1; - - @UnsupportedAppUsage - private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>(); - private SparseArray<FileDescriptorRecord> mFileDescriptorRecords; - private IdleHandler[] mPendingIdleHandlers; - private boolean mQuitting; - - // Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout. - private boolean mBlocked; - - // The next barrier token. - // Barriers are indicated by messages with a null target whose arg1 field carries the token. - @UnsupportedAppUsage - private int mNextBarrierToken; - - @RavenwoodRedirect - private native static long nativeInit(); - @RavenwoodRedirect - private native static void nativeDestroy(long ptr); - @UnsupportedAppUsage - @RavenwoodRedirect - private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ - @RavenwoodRedirect - private native static void nativeWake(long ptr); - @RavenwoodRedirect - private native static boolean nativeIsPolling(long ptr); - @RavenwoodRedirect - private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events); - - MessageQueue(boolean quitAllowed) { - mQuitAllowed = quitAllowed; - mPtr = nativeInit(); - } - - @GuardedBy("this") - private void removeRootFromPriorityQueue(Message msg) { - Message tmp; - if (msg.isAsynchronous()) { - tmp = mAsyncPriorityQueue.poll(); - } else { - tmp = mPriorityQueue.poll(); - } - if (DEBUG && tmp != msg) { - Log.wtf(TAG, "Unexpected message at head of heap. Wanted: " + msg + " msg.isAsync " - + msg.isAsynchronous() + " Found: " + tmp); - - mPriorityQueue.print(); - mAsyncPriorityQueue.print(); - } - } - - @GuardedBy("this") - private Message pickEarliestMessage(Message x, Message y) { - if (x != null && y != null) { - if (mPriorityQueue.compareMessage(x, y) < 0) { - return x; - } - return y; - } - - return x != null ? x : y; - } - - @GuardedBy("this") - private Message peekEarliestMessage() { - Message x = mPriorityQueue.peek(); - Message y = mAsyncPriorityQueue.peek(); - - return pickEarliestMessage(x, y); - } - - @GuardedBy("this") - private boolean priorityQueuesAreEmpty() { - return mPriorityQueue.isEmpty() && mAsyncPriorityQueue.isEmpty(); - } - - @GuardedBy("this") - private boolean priorityQueueHasBarrier() { - Message m = mPriorityQueue.peek(); - - if (m != null && m.target == null) { - return true; - } - return false; - } - - @Override - protected void finalize() throws Throwable { - try { - dispose(); - } finally { - super.finalize(); - } - } - - // Disposes of the underlying message queue. - // Must only be called on the looper thread or the finalizer. - private void dispose() { - if (mPtr != 0) { - nativeDestroy(mPtr); - mPtr = 0; - } - } - - /** - * Returns true if the looper has no pending messages which are due to be processed. - * - * <p>This method is safe to call from any thread. - * - * @return True if the looper is idle. - */ - public boolean isIdle() { - synchronized (this) { - Message m = peekEarliestMessage(); - final long now = SystemClock.uptimeMillis(); - - return (priorityQueuesAreEmpty() || now < m.when); - } - } - - /** - * Add a new {@link IdleHandler} to this message queue. This may be - * removed automatically for you by returning false from - * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is - * invoked, or explicitly removing it with {@link #removeIdleHandler}. - * - * <p>This method is safe to call from any thread. - * - * @param handler The IdleHandler to be added. - */ - public void addIdleHandler(@NonNull IdleHandler handler) { - if (handler == null) { - throw new NullPointerException("Can't add a null IdleHandler"); - } - synchronized (this) { - mIdleHandlers.add(handler); - } - } - - /** - * Remove an {@link IdleHandler} from the queue that was previously added - * with {@link #addIdleHandler}. If the given object is not currently - * in the idle list, nothing is done. - * - * <p>This method is safe to call from any thread. - * - * @param handler The IdleHandler to be removed. - */ - public void removeIdleHandler(@NonNull IdleHandler handler) { - synchronized (this) { - mIdleHandlers.remove(handler); - } - } - - /** - * Returns whether this looper's thread is currently polling for more work to do. - * This is a good signal that the loop is still alive rather than being stuck - * handling a callback. Note that this method is intrinsically racy, since the - * state of the loop can change before you get the result back. - * - * <p>This method is safe to call from any thread. - * - * @return True if the looper is currently polling for events. - * @hide - */ - public boolean isPolling() { - synchronized (this) { - return isPollingLocked(); - } - } - - private boolean isPollingLocked() { - // If the loop is quitting then it must not be idling. - // We can assume mPtr != 0 when mQuitting is false. - return !mQuitting && nativeIsPolling(mPtr); - } - - /** - * Adds a file descriptor listener to receive notification when file descriptor - * related events occur. - * <p> - * If the file descriptor has already been registered, the specified events - * and listener will replace any that were previously associated with it. - * It is not possible to set more than one listener per file descriptor. - * </p><p> - * It is important to always unregister the listener when the file descriptor - * is no longer of use. - * </p> - * - * @param fd The file descriptor for which a listener will be registered. - * @param events The set of events to receive: a combination of the - * {@link OnFileDescriptorEventListener#EVENT_INPUT}, - * {@link OnFileDescriptorEventListener#EVENT_OUTPUT}, and - * {@link OnFileDescriptorEventListener#EVENT_ERROR} event masks. If the requested - * set of events is zero, then the listener is unregistered. - * @param listener The listener to invoke when file descriptor events occur. - * - * @see OnFileDescriptorEventListener - * @see #removeOnFileDescriptorEventListener - */ - @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class) - public void addOnFileDescriptorEventListener(@NonNull FileDescriptor fd, - @OnFileDescriptorEventListener.Events int events, - @NonNull OnFileDescriptorEventListener listener) { - if (fd == null) { - throw new IllegalArgumentException("fd must not be null"); - } - if (listener == null) { - throw new IllegalArgumentException("listener must not be null"); - } - - synchronized (this) { - updateOnFileDescriptorEventListenerLocked(fd, events, listener); - } - } - - /** - * Removes a file descriptor listener. - * <p> - * This method does nothing if no listener has been registered for the - * specified file descriptor. - * </p> - * - * @param fd The file descriptor whose listener will be unregistered. - * - * @see OnFileDescriptorEventListener - * @see #addOnFileDescriptorEventListener - */ - @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class) - public void removeOnFileDescriptorEventListener(@NonNull FileDescriptor fd) { - if (fd == null) { - throw new IllegalArgumentException("fd must not be null"); - } - - synchronized (this) { - updateOnFileDescriptorEventListenerLocked(fd, 0, null); - } - } - - @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class) - private void updateOnFileDescriptorEventListenerLocked(FileDescriptor fd, int events, - OnFileDescriptorEventListener listener) { - final int fdNum = fd.getInt$(); - - int index = -1; - FileDescriptorRecord record = null; - if (mFileDescriptorRecords != null) { - index = mFileDescriptorRecords.indexOfKey(fdNum); - if (index >= 0) { - record = mFileDescriptorRecords.valueAt(index); - if (record != null && record.mEvents == events) { - return; - } - } - } - - if (events != 0) { - events |= OnFileDescriptorEventListener.EVENT_ERROR; - if (record == null) { - if (mFileDescriptorRecords == null) { - mFileDescriptorRecords = new SparseArray<FileDescriptorRecord>(); - } - record = new FileDescriptorRecord(fd, events, listener); - mFileDescriptorRecords.put(fdNum, record); - } else { - record.mListener = listener; - record.mEvents = events; - record.mSeq += 1; - } - nativeSetFileDescriptorEvents(mPtr, fdNum, events); - } else if (record != null) { - record.mEvents = 0; - mFileDescriptorRecords.removeAt(index); - nativeSetFileDescriptorEvents(mPtr, fdNum, 0); - } - } - - // Called from native code. - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - private int dispatchEvents(int fd, int events) { - // Get the file descriptor record and any state that might change. - final FileDescriptorRecord record; - final int oldWatchedEvents; - final OnFileDescriptorEventListener listener; - final int seq; - synchronized (this) { - record = mFileDescriptorRecords.get(fd); - if (record == null) { - return 0; // spurious, no listener registered - } - - oldWatchedEvents = record.mEvents; - events &= oldWatchedEvents; // filter events based on current watched set - if (events == 0) { - return oldWatchedEvents; // spurious, watched events changed - } - - listener = record.mListener; - seq = record.mSeq; - } - - // Invoke the listener outside of the lock. - int newWatchedEvents = listener.onFileDescriptorEvents( - record.mDescriptor, events); - if (newWatchedEvents != 0) { - newWatchedEvents |= OnFileDescriptorEventListener.EVENT_ERROR; - } - - // Update the file descriptor record if the listener changed the set of - // events to watch and the listener itself hasn't been updated since. - if (newWatchedEvents != oldWatchedEvents) { - synchronized (this) { - int index = mFileDescriptorRecords.indexOfKey(fd); - if (index >= 0 && mFileDescriptorRecords.valueAt(index) == record - && record.mSeq == seq) { - record.mEvents = newWatchedEvents; - if (newWatchedEvents == 0) { - mFileDescriptorRecords.removeAt(index); - } - } - } - } - - // Return the new set of events to watch for native code to take care of. - return newWatchedEvents; - } - - private static final AtomicLong mMessagesDelivered = new AtomicLong(); - - @UnsupportedAppUsage - Message next() { - // Return here if the message loop has already quit and been disposed. - // This can happen if the application tries to restart a looper after quit - // which is not supported. - final long ptr = mPtr; - if (ptr == 0) { - return null; - } - - int pendingIdleHandlerCount = -1; // -1 only during first iteration - int nextPollTimeoutMillis = 0; - for (;;) { - if (nextPollTimeoutMillis != 0) { - Binder.flushPendingCommands(); - } - - nativePollOnce(ptr, nextPollTimeoutMillis); - - synchronized (this) { - // Try to retrieve the next message. Return if found. - final long now = SystemClock.uptimeMillis(); - Message prevMsg = null; - Message msg = peekEarliestMessage(); - - if (DEBUG && msg != null) { - Log.v(TAG, "Next found message " + msg + " isAsynchronous: " - + msg.isAsynchronous() + " target " + msg.target); - } - - if (msg != null && !msg.isAsynchronous() && msg.target == null) { - // Stalled by a barrier. Find the next asynchronous message in the queue. - msg = mAsyncPriorityQueue.peek(); - if (DEBUG) { - Log.v(TAG, "Next message was barrier async msg: " + msg); - } - } - - if (msg != null) { - if (now < msg.when) { - // Next message is not ready. Set a timeout to wake up when it is ready. - nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); - } else { - mBlocked = false; - removeRootFromPriorityQueue(msg); - if (DEBUG) Log.v(TAG, "Returning message: " + msg); - msg.markInUse(); - if (TRACE) { - Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet()); - } - return msg; - } - } else { - // No more messages. - nextPollTimeoutMillis = -1; - } - - // Process the quit message now that all pending messages have been handled. - if (mQuitting) { - dispose(); - return null; - } - - // If first time idle, then get the number of idlers to run. - // Idle handles only run if the queue is empty or if the first message - // in the queue (possibly a barrier) is due to be handled in the future. - Message next = peekEarliestMessage(); - if (pendingIdleHandlerCount < 0 - && (next == null || now < next.when)) { - pendingIdleHandlerCount = mIdleHandlers.size(); - } - if (pendingIdleHandlerCount <= 0) { - // No idle handlers to run. Loop and wait some more. - mBlocked = true; - continue; - } - - if (mPendingIdleHandlers == null) { - mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; - } - mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); - } - - // Run the idle handlers. - // We only ever reach this code block during the first iteration. - for (int i = 0; i < pendingIdleHandlerCount; i++) { - final IdleHandler idler = mPendingIdleHandlers[i]; - mPendingIdleHandlers[i] = null; // release the reference to the handler - - boolean keep = false; - try { - keep = idler.queueIdle(); - } catch (Throwable t) { - Log.wtf(TAG, "IdleHandler threw exception", t); - } - - if (!keep) { - synchronized (this) { - mIdleHandlers.remove(idler); - } - } - } - - // Reset the idle handler count to 0 so we do not run them again. - pendingIdleHandlerCount = 0; - - // While calling an idle handler, a new message could have been delivered - // so go back and look again for a pending message without waiting. - nextPollTimeoutMillis = 0; - } - } - - void quit(boolean safe) { - if (!mQuitAllowed) { - throw new IllegalStateException("Main thread not allowed to quit."); - } - - synchronized (this) { - if (mQuitting) { - return; - } - mQuitting = true; - - if (safe) { - removeAllFutureMessagesLocked(); - } else { - removeAllMessagesLocked(); - } - - // We can assume mPtr != 0 because mQuitting was previously false. - nativeWake(mPtr); - } - } - - /** - * Posts a synchronization barrier to the Looper's message queue. - * - * Message processing occurs as usual until the message queue encounters the - * synchronization barrier that has been posted. When the barrier is encountered, - * later synchronous messages in the queue are stalled (prevented from being executed) - * until the barrier is released by calling {@link #removeSyncBarrier} and specifying - * the token that identifies the synchronization barrier. - * - * This method is used to immediately postpone execution of all subsequently posted - * synchronous messages until a condition is met that releases the barrier. - * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier - * and continue to be processed as usual. - * - * This call must be always matched by a call to {@link #removeSyncBarrier} with - * the same token to ensure that the message queue resumes normal operation. - * Otherwise the application will probably hang! - * - * @return A token that uniquely identifies the barrier. This token must be - * passed to {@link #removeSyncBarrier} to release the barrier. - * - * @hide - */ - @UnsupportedAppUsage - @TestApi - public int postSyncBarrier() { - return postSyncBarrier(SystemClock.uptimeMillis()); - } - - private int postSyncBarrier(long when) { - // Enqueue a new sync barrier token. - // We don't need to wake the queue because the purpose of a barrier is to stall it. - synchronized (this) { - final int token = mNextBarrierToken++; - final Message msg = Message.obtain(); - msg.arg1 = token; - - enqueueMessageUnchecked(msg, when); - return token; - } - } - - private class MatchBarrierToken extends MessageHeap.MessageHeapCompare { - int mBarrierToken; - - MatchBarrierToken(int token) { - super(); - mBarrierToken = token; - } - - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == null && m.arg1 == mBarrierToken) { - return true; - } - return false; - } - } - - /** - * Removes a synchronization barrier. - * - * @param token The synchronization barrier token that was returned by - * {@link #postSyncBarrier}. - * - * @throws IllegalStateException if the barrier was not found. - * - * @hide - */ - @UnsupportedAppUsage - @TestApi - public void removeSyncBarrier(int token) { - final MatchBarrierToken matchBarrierToken = new MatchBarrierToken(token); - - // Remove a sync barrier token from the queue. - // If the queue is no longer stalled by a barrier then wake it. - synchronized (this) { - boolean removed; - Message first = mPriorityQueue.peek(); - - removed = mPriorityQueue.findOrRemoveMessages(null, 0, null, null, 0, - matchBarrierToken, true); - if (removed && first != null) { - // If the loop is quitting then it is already awake. - // We can assume mPtr != 0 when mQuitting is false. - if (first.target == null && first.arg1 == token && !mQuitting) { - nativeWake(mPtr); - } - } else if (!removed) { - throw new IllegalStateException("The specified message queue synchronization " - + " barrier token has not been posted or has already been removed."); - } - } - } - - boolean enqueueMessage(Message msg, long when) { - if (msg.target == null) { - throw new IllegalArgumentException("Message must have a target."); - } - - return enqueueMessageUnchecked(msg, when); - } - - boolean enqueueMessageUnchecked(Message msg, long when) { - synchronized (this) { - if (mQuitting) { - IllegalStateException e = new IllegalStateException( - msg.target + " sending message to a Handler on a dead thread"); - Log.w(TAG, e.getMessage(), e); - msg.recycle(); - return false; - } - - if (msg.isInUse()) { - throw new IllegalStateException(msg + " This message is already in use."); - } - - msg.markInUse(); - msg.when = when; - msg.mInsertSeq = when != 0 ? mNextInsertSeq++ : mNextFrontInsertSeq--; - if (DEBUG) Log.v(TAG, "Enqueue message: " + msg); - boolean needWake; - boolean isBarrier = msg.target == null; - Message first = peekEarliestMessage(); - - if (priorityQueuesAreEmpty() || when == 0 || when < first.when) { - needWake = mBlocked && !isBarrier; - } else { - Message firstNonAsyncMessage = - first.isAsynchronous() ? mPriorityQueue.peek() : first; - - needWake = mBlocked && firstNonAsyncMessage != null - && firstNonAsyncMessage.target == null && msg.isAsynchronous(); - } - - if (msg.isAsynchronous()) { - mAsyncPriorityQueue.add(msg); - } else { - mPriorityQueue.add(msg); - } - - // We can assume mPtr != 0 because mQuitting is false. - if (needWake) { - nativeWake(mPtr); - } - } - return true; - } - - @GuardedBy("this") - boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when, - MessageHeap.MessageHeapCompare compare, boolean removeMatches) { - boolean found = mPriorityQueue.findOrRemoveMessages(h, what, object, r, when, compare, - removeMatches); - boolean foundAsync = mAsyncPriorityQueue.findOrRemoveMessages(h, what, object, r, when, - compare, removeMatches); - return found || foundAsync; - } - - private static class MatchHandlerWhatAndObject extends MessageHeap.MessageHeapCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && m.what == what && (object == null || m.obj == object)) { - return true; - } - return false; - } - } - private static final MatchHandlerWhatAndObject sMatchHandlerWhatAndObject = - new MatchHandlerWhatAndObject(); - - boolean hasMessages(Handler h, int what, Object object) { - if (h == null) { - return false; - } - - synchronized (this) { - return findOrRemoveMessages(h, what, object, null, 0, sMatchHandlerWhatAndObject, - false); - } - } - - private static class MatchHandlerWhatAndObjectEquals extends MessageHeap.MessageHeapCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && m.what == what && (object == null || object.equals(m.obj))) { - return true; - } - return false; - } - } - private static final MatchHandlerWhatAndObjectEquals sMatchHandlerWhatAndObjectEquals = - new MatchHandlerWhatAndObjectEquals(); - boolean hasEqualMessages(Handler h, int what, Object object) { - if (h == null) { - return false; - } - - synchronized (this) { - return findOrRemoveMessages(h, what, object, null, 0, - sMatchHandlerWhatAndObjectEquals, false); - } - } - - private static class MatchHandlerRunnableAndObject extends MessageHeap.MessageHeapCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && m.callback == r && (object == null || m.obj == object)) { - return true; - } - return false; - } - } - private static final MatchHandlerRunnableAndObject sMatchHandlerRunnableAndObject = - new MatchHandlerRunnableAndObject(); - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - boolean hasMessages(Handler h, Runnable r, Object object) { - if (h == null) { - return false; - } - - synchronized (this) { - return findOrRemoveMessages(h, -1, object, r, 0, sMatchHandlerRunnableAndObject, - false); - } - } - - private static class MatchHandler extends MessageHeap.MessageHeapCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h) { - return true; - } - return false; - } - } - private static final MatchHandler sMatchHandler = new MatchHandler(); - boolean hasMessages(Handler h) { - if (h == null) { - return false; - } - - synchronized (this) { - return findOrRemoveMessages(h, -1, null, null, 0, sMatchHandler, false); - } - } - - void removeMessages(Handler h, int what, Object object) { - if (h == null) { - return; - } - - synchronized (this) { - findOrRemoveMessages(h, what, object, null, 0, sMatchHandlerWhatAndObject, true); - } - } - - void removeEqualMessages(Handler h, int what, Object object) { - if (h == null) { - return; - } - - synchronized (this) { - findOrRemoveMessages(h, what, object, null, 0, sMatchHandlerWhatAndObjectEquals, true); - } - } - - void removeMessages(Handler h, Runnable r, Object object) { - if (h == null || r == null) { - return; - } - - synchronized (this) { - findOrRemoveMessages(h, -1, object, r, 0, sMatchHandlerRunnableAndObject, true); - } - } - - private static class MatchHandlerRunnableAndObjectEquals - extends MessageHeap.MessageHeapCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && m.callback == r && (object == null || object.equals(m.obj))) { - return true; - } - return false; - } - } - private static final MatchHandlerRunnableAndObjectEquals sMatchHandlerRunnableAndObjectEquals = - new MatchHandlerRunnableAndObjectEquals(); - void removeEqualMessages(Handler h, Runnable r, Object object) { - if (h == null || r == null) { - return; - } - - synchronized (this) { - findOrRemoveMessages(h, -1, object, r, 0, sMatchHandlerRunnableAndObjectEquals, true); - } - } - - private static class MatchHandlerAndObject extends MessageHeap.MessageHeapCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && (object == null || m.obj == object)) { - return true; - } - return false; - } - } - private static final MatchHandlerAndObject sMatchHandlerAndObject = new MatchHandlerAndObject(); - void removeCallbacksAndMessages(Handler h, Object object) { - if (h == null) { - return; - } - - synchronized (this) { - findOrRemoveMessages(h, -1, object, null, 0, sMatchHandlerAndObject, true); - } - } - - private static class MatchHandlerAndObjectEquals extends MessageHeap.MessageHeapCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && (object == null || object.equals(m.obj))) { - return true; - } - return false; - } - } - private static final MatchHandlerAndObjectEquals sMatchHandlerAndObjectEquals = - new MatchHandlerAndObjectEquals(); - void removeCallbacksAndEqualMessages(Handler h, Object object) { - if (h == null) { - return; - } - - synchronized (this) { - findOrRemoveMessages(h, -1, object, null, 0, sMatchHandlerAndObjectEquals, true); - } - } - - @GuardedBy("this") - private void removeAllMessagesLocked() { - mPriorityQueue.removeAll(); - mAsyncPriorityQueue.removeAll(); - } - - private static class MatchAllFutureMessages extends MessageHeap.MessageHeapCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.when > when) { - return true; - } - return false; - } - } - private static final MatchAllFutureMessages sMatchAllFutureMessages = - new MatchAllFutureMessages(); - @GuardedBy("this") - private void removeAllFutureMessagesLocked() { - findOrRemoveMessages(null, -1, null, null, SystemClock.uptimeMillis(), - sMatchAllFutureMessages, true); - } - - @NeverCompile - int dumpPriorityQueue(Printer pw, String prefix, Handler h, MessageHeap priorityQueue) { - int n = 0; - long now = SystemClock.uptimeMillis(); - for (int i = 0; i < priorityQueue.numElements(); i++) { - Message m = priorityQueue.getMessageAt(i); - if (h == null && h == m.target) { - pw.println(prefix + "Message " + n + ": " + m.toString(now)); - n++; - } - } - return n; - } - - @NeverCompile - void dumpPriorityQueue(ProtoOutputStream proto, MessageHeap priorityQueue) { - for (int i = 0; i < priorityQueue.numElements(); i++) { - Message m = priorityQueue.getMessageAt(i); - m.dumpDebug(proto, MessageQueueProto.MESSAGES); - } - } - - @NeverCompile - void dump(Printer pw, String prefix, Handler h) { - synchronized (this) { - pw.println(prefix + "(MessageQueue is using Locked implementation)"); - long now = SystemClock.uptimeMillis(); - int n = dumpPriorityQueue(pw, prefix, h, mPriorityQueue); - n += dumpPriorityQueue(pw, prefix, h, mAsyncPriorityQueue); - pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked() - + ", quitting=" + mQuitting + ")"); - } - } - - @NeverCompile - void dumpDebug(ProtoOutputStream proto, long fieldId) { - final long messageQueueToken = proto.start(fieldId); - synchronized (this) { - dumpPriorityQueue(proto, mPriorityQueue); - dumpPriorityQueue(proto, mAsyncPriorityQueue); - proto.write(MessageQueueProto.IS_POLLING_LOCKED, isPollingLocked()); - proto.write(MessageQueueProto.IS_QUITTING, mQuitting); - } - proto.end(messageQueueToken); - } - - /** - * Callback interface for discovering when a thread is going to block - * waiting for more messages. - */ - public static interface IdleHandler { - /** - * Called when the message queue has run out of messages and will now - * wait for more. Return true to keep your idle handler active, false - * to have it removed. This may be called if there are still messages - * pending in the queue, but they are all scheduled to be dispatched - * after the current time. - */ - boolean queueIdle(); - } - - /** - * A listener which is invoked when file descriptor related events occur. - */ - public interface OnFileDescriptorEventListener { - /** - * File descriptor event: Indicates that the file descriptor is ready for input - * operations, such as reading. - * <p> - * The listener should read all available data from the file descriptor - * then return <code>true</code> to keep the listener active or <code>false</code> - * to remove the listener. - * </p><p> - * In the case of a socket, this event may be generated to indicate - * that there is at least one incoming connection that the listener - * should accept. - * </p><p> - * This event will only be generated if the {@link #EVENT_INPUT} event mask was - * specified when the listener was added. - * </p> - */ - public static final int EVENT_INPUT = 1 << 0; - - /** - * File descriptor event: Indicates that the file descriptor is ready for output - * operations, such as writing. - * <p> - * The listener should write as much data as it needs. If it could not - * write everything at once, then it should return <code>true</code> to - * keep the listener active. Otherwise, it should return <code>false</code> - * to remove the listener then re-register it later when it needs to write - * something else. - * </p><p> - * This event will only be generated if the {@link #EVENT_OUTPUT} event mask was - * specified when the listener was added. - * </p> - */ - public static final int EVENT_OUTPUT = 1 << 1; - - /** - * File descriptor event: Indicates that the file descriptor encountered a - * fatal error. - * <p> - * File descriptor errors can occur for various reasons. One common error - * is when the remote peer of a socket or pipe closes its end of the connection. - * </p><p> - * This event may be generated at any time regardless of whether the - * {@link #EVENT_ERROR} event mask was specified when the listener was added. - * </p> - */ - public static final int EVENT_ERROR = 1 << 2; - - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, prefix = { "EVENT_" }, value = { - EVENT_INPUT, - EVENT_OUTPUT, - EVENT_ERROR - }) - public @interface Events {} - - /** - * Called when a file descriptor receives events. - * - * @param fd The file descriptor. - * @param events The set of events that occurred: a combination of the - * {@link #EVENT_INPUT}, {@link #EVENT_OUTPUT}, and {@link #EVENT_ERROR} event masks. - * @return The new set of events to watch, or 0 to unregister the listener. - * - * @see #EVENT_INPUT - * @see #EVENT_OUTPUT - * @see #EVENT_ERROR - */ - @Events int onFileDescriptorEvents(@NonNull FileDescriptor fd, @Events int events); - } - - private static final class FileDescriptorRecord { - public final FileDescriptor mDescriptor; - public int mEvents; - public OnFileDescriptorEventListener mListener; - public int mSeq; - - public FileDescriptorRecord(FileDescriptor descriptor, - int events, OnFileDescriptorEventListener listener) { - mDescriptor = descriptor; - mEvents = events; - mListener = listener; - } - } -} diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java index d16e4473d55f..1329b90538bb 100644 --- a/core/java/android/os/Looper.java +++ b/core/java/android/os/Looper.java @@ -200,7 +200,11 @@ public final class Looper { } PerfettoTrace.begin(PerfettoTrace.MQ_CATEGORY, "message_queue_receive") - .addArg("sending_thread", msg.mSendingThreadName) + .beginProto() + .beginNested(2004 /* message_queue */) + .addField(1 /* sending_thread_name */, msg.mSendingThreadName) + .endNested() + .endProto() .addTerminatingFlow(msg.mEventId.get()) .emit(); diff --git a/core/java/android/os/PerfettoTrace.java b/core/java/android/os/PerfettoTrace.java index 741d542ecb3b..932836f8a050 100644 --- a/core/java/android/os/PerfettoTrace.java +++ b/core/java/android/os/PerfettoTrace.java @@ -232,10 +232,6 @@ public final class PerfettoTrace { * @param eventName The event name to appear in the trace. */ public static PerfettoTrackEventExtra.Builder instant(Category category, String eventName) { - if (!category.isEnabled()) { - return PerfettoTrackEventExtra.noOpBuilder(); - } - return PerfettoTrackEventExtra.builder().init(PERFETTO_TE_TYPE_INSTANT, category) .setEventName(eventName); } @@ -247,10 +243,6 @@ public final class PerfettoTrace { * @param eventName The event name to appear in the trace. */ public static PerfettoTrackEventExtra.Builder begin(Category category, String eventName) { - if (!category.isEnabled()) { - return PerfettoTrackEventExtra.noOpBuilder(); - } - return PerfettoTrackEventExtra.builder().init(PERFETTO_TE_TYPE_SLICE_BEGIN, category) .setEventName(eventName); } @@ -261,10 +253,6 @@ public final class PerfettoTrace { * @param category The perfetto category. */ public static PerfettoTrackEventExtra.Builder end(Category category) { - if (!category.isEnabled()) { - return PerfettoTrackEventExtra.noOpBuilder(); - } - return PerfettoTrackEventExtra.builder().init(PERFETTO_TE_TYPE_SLICE_END, category); } @@ -275,10 +263,6 @@ public final class PerfettoTrace { * @param value The value of the counter. */ public static PerfettoTrackEventExtra.Builder counter(Category category, long value) { - if (!category.isEnabled()) { - return PerfettoTrackEventExtra.noOpBuilder(); - } - return PerfettoTrackEventExtra.builder().init(PERFETTO_TE_TYPE_COUNTER, category) .setCounter(value); } @@ -302,10 +286,6 @@ public final class PerfettoTrace { * @param value The value of the counter. */ public static PerfettoTrackEventExtra.Builder counter(Category category, double value) { - if (!category.isEnabled()) { - return PerfettoTrackEventExtra.noOpBuilder(); - } - return PerfettoTrackEventExtra.builder().init(PERFETTO_TE_TYPE_COUNTER, category) .setCounter(value); } diff --git a/core/java/android/os/PerfettoTrackEventExtra.java b/core/java/android/os/PerfettoTrackEventExtra.java index adb98aa25f8f..f4b5dfe76f88 100644 --- a/core/java/android/os/PerfettoTrackEventExtra.java +++ b/core/java/android/os/PerfettoTrackEventExtra.java @@ -23,6 +23,8 @@ import dalvik.annotation.optimization.FastNative; import libcore.util.NativeAllocationRegistry; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; @@ -33,8 +35,8 @@ import java.util.function.Supplier; */ @android.ravenwood.annotation.RavenwoodKeepWholeClass public final class PerfettoTrackEventExtra { + private static final boolean DEBUG = false; private static final int DEFAULT_EXTRA_CACHE_SIZE = 5; - private static final Builder NO_OP_BUILDER = new NoOpBuilder(); private static final ThreadLocal<PerfettoTrackEventExtra> sTrackEventExtra = new ThreadLocal<PerfettoTrackEventExtra>() { @Override @@ -44,15 +46,18 @@ public final class PerfettoTrackEventExtra { }; private static final AtomicLong sNamedTrackId = new AtomicLong(); private static final Supplier<Flow> sFlowSupplier = Flow::new; - private static final Supplier<BuilderImpl> sBuilderSupplier = BuilderImpl::new; + private static final Supplier<Builder> sBuilderSupplier = Builder::new; private static final Supplier<FieldInt64> sFieldInt64Supplier = FieldInt64::new; private static final Supplier<FieldDouble> sFieldDoubleSupplier = FieldDouble::new; private static final Supplier<FieldString> sFieldStringSupplier = FieldString::new; private static final Supplier<FieldNested> sFieldNestedSupplier = FieldNested::new; + private final List<PerfettoPointer> mPendingPointers = new ArrayList<>(); private CounterInt64 mCounterInt64; private CounterDouble mCounterDouble; private Proto mProto; + private Flow mFlow; + private Flow mTerminatingFlow; /** * Represents a native pointer to a Perfetto C SDK struct. E.g. PerfettoTeHlExtra. @@ -132,245 +137,10 @@ public final class PerfettoTrackEventExtra { } } - public interface Builder { - /** - * Emits the track event. - */ - void emit(); - - /** - * Initialize the builder for a new trace event. - */ - Builder init(int traceType, PerfettoTrace.Category category); - - /** - * Sets the event name for the track event. - */ - Builder setEventName(String eventName); - - /** - * Adds a debug arg with key {@code name} and value {@code val}. - */ - Builder addArg(String name, long val); - - /** - * Adds a debug arg with key {@code name} and value {@code val}. - */ - Builder addArg(String name, boolean val); - - /** - * Adds a debug arg with key {@code name} and value {@code val}. - */ - Builder addArg(String name, double val); - - /** - * Adds a debug arg with key {@code name} and value {@code val}. - */ - Builder addArg(String name, String val); - - /** - * Adds a flow with {@code id}. - */ - Builder addFlow(int id); - - /** - * Adds a terminating flow with {@code id}. - */ - Builder addTerminatingFlow(int id); - - /** - * Adds the events to a named track instead of the thread track where the - * event occurred. - */ - Builder usingNamedTrack(long parentUuid, String name); - - /** - * Adds the events to a process scoped named track instead of the thread track where the - * event occurred. - */ - Builder usingProcessNamedTrack(String name); - - /** - * Adds the events to a thread scoped named track instead of the thread track where the - * event occurred. - */ - Builder usingThreadNamedTrack(long tid, String name); - - /** - * Adds the events to a counter track instead. This is required for - * setting counter values. - */ - Builder usingCounterTrack(long parentUuid, String name); - - /** - * Adds the events to a process scoped counter track instead. This is required for - * setting counter values. - */ - Builder usingProcessCounterTrack(String name); - - /** - * Adds the events to a thread scoped counter track instead. This is required for - * setting counter values. - */ - Builder usingThreadCounterTrack(long tid, String name); - - /** - * Sets a long counter value on the event. - * - */ - Builder setCounter(long val); - - /** - * Sets a double counter value on the event. - * - */ - Builder setCounter(double val); - - /** - * Adds a proto field with field id {@code id} and value {@code val}. - */ - Builder addField(long id, long val); - - /** - * Adds a proto field with field id {@code id} and value {@code val}. - */ - Builder addField(long id, double val); - - /** - * Adds a proto field with field id {@code id} and value {@code val}. - */ - Builder addField(long id, String val); - - /** - * Begins a proto field with field - * Fields can be added from this point and there must be a corresponding - * {@link endProto}. - * - * The proto field is a singleton and all proto fields get added inside the - * one {@link beginProto} and {@link endProto} within the {@link Builder}. - */ - Builder beginProto(); - - /** - * Ends a proto field. - */ - Builder endProto(); - - /** - * Begins a nested proto field with field id {@code id}. - * Fields can be added from this point and there must be a corresponding - * {@link endNested}. - */ - Builder beginNested(long id); - - /** - * Ends a nested proto field. - */ - Builder endNested(); - } - - @android.ravenwood.annotation.RavenwoodKeepWholeClass - public static final class NoOpBuilder implements Builder { - @Override - public void emit() {} - @Override - public Builder init(int traceType, PerfettoTrace.Category category) { - return this; - } - @Override - public Builder setEventName(String eventName) { - return this; - } - @Override - public Builder addArg(String name, long val) { - return this; - } - @Override - public Builder addArg(String name, boolean val) { - return this; - } - @Override - public Builder addArg(String name, double val) { - return this; - } - @Override - public Builder addArg(String name, String val) { - return this; - } - @Override - public Builder addFlow(int id) { - return this; - } - @Override - public Builder addTerminatingFlow(int id) { - return this; - } - @Override - public Builder usingNamedTrack(long parentUuid, String name) { - return this; - } - @Override - public Builder usingProcessNamedTrack(String name) { - return this; - } - @Override - public Builder usingThreadNamedTrack(long tid, String name) { - return this; - } - @Override - public Builder usingCounterTrack(long parentUuid, String name) { - return this; - } - @Override - public Builder usingProcessCounterTrack(String name) { - return this; - } - @Override - public Builder usingThreadCounterTrack(long tid, String name) { - return this; - } - @Override - public Builder setCounter(long val) { - return this; - } - @Override - public Builder setCounter(double val) { - return this; - } - @Override - public Builder addField(long id, long val) { - return this; - } - @Override - public Builder addField(long id, double val) { - return this; - } - @Override - public Builder addField(long id, String val) { - return this; - } - @Override - public Builder beginProto() { - return this; - } - @Override - public Builder endProto() { - return this; - } - @Override - public Builder beginNested(long id) { - return this; - } - @Override - public Builder endNested() { - return this; - } - } - /** * Builder for Perfetto track event extras. */ - public static final class BuilderImpl implements Builder { + public static final class Builder { // For performance reasons, we hold a reference to mExtra as a holder for // perfetto pointers being added. This way, we avoid an additional list to hold // the pointers in Java and we can pass them down directly to native code. @@ -383,10 +153,13 @@ public final class PerfettoTrackEventExtra { private Builder mParent; private FieldContainer mCurrentContainer; + private boolean mIsCategoryEnabled; private final CounterInt64 mCounterInt64; private final CounterDouble mCounterDouble; private final Proto mProto; + private final Flow mFlow; + private final Flow mTerminatingFlow; private final RingBuffer<NamedTrack> mNamedTrackCache; private final RingBuffer<CounterTrack> mCounterTrackCache; @@ -400,9 +173,9 @@ public final class PerfettoTrackEventExtra { private final Pool<FieldString> mFieldStringCache; private final Pool<FieldNested> mFieldNestedCache; private final Pool<Flow> mFlowCache; - private final Pool<BuilderImpl> mBuilderCache; + private final Pool<Builder> mBuilderCache; - private BuilderImpl() { + private Builder() { mExtra = sTrackEventExtra.get(); mNamedTrackCache = mExtra.mNamedTrackCache; mCounterTrackCache = mExtra.mCounterTrackCache; @@ -420,20 +193,32 @@ public final class PerfettoTrackEventExtra { mCounterInt64 = mExtra.getCounterInt64(); mCounterDouble = mExtra.getCounterDouble(); mProto = mExtra.getProto(); + mFlow = mExtra.getFlow(); + mTerminatingFlow = mExtra.getTerminatingFlow(); } - @Override + /** + * Emits the track event. + */ public void emit() { - checkParent(); - mIsBuilt = true; + if (!mIsCategoryEnabled) { + return; + } + if (DEBUG) { + checkParent(); + } + mIsBuilt = true; native_emit(mTraceType, mCategory.getPtr(), mEventName, mExtra.getPtr()); - // Reset after emitting to free any the extras used to trace the event. - mExtra.reset(); } - @Override + /** + * Initialize the builder for a new trace event. + */ public Builder init(int traceType, PerfettoTrace.Category category) { + if (!category.isEnabled()) { + return this; + } mTraceType = traceType; mCategory = category; mEventName = ""; @@ -446,18 +231,27 @@ public final class PerfettoTrackEventExtra { mExtra.reset(); // Reset after on init in case the thread created builders without calling emit - return initInternal(this, null); + return initInternal(this, null, true); } - @Override + /** + * Sets the event name for the track event. + */ public Builder setEventName(String eventName) { mEventName = eventName; return this; } - @Override + /** + * Adds a debug arg with key {@code name} and value {@code val}. + */ public Builder addArg(String name, long val) { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } ArgInt64 arg = mArgInt64Cache.get(name.hashCode()); if (arg == null || !arg.getName().equals(name)) { arg = new ArgInt64(name); @@ -468,9 +262,16 @@ public final class PerfettoTrackEventExtra { return this; } - @Override + /** + * Adds a debug arg with key {@code name} and value {@code val}. + */ public Builder addArg(String name, boolean val) { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } ArgBool arg = mArgBoolCache.get(name.hashCode()); if (arg == null || !arg.getName().equals(name)) { arg = new ArgBool(name); @@ -481,9 +282,16 @@ public final class PerfettoTrackEventExtra { return this; } - @Override + /** + * Adds a debug arg with key {@code name} and value {@code val}. + */ public Builder addArg(String name, double val) { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } ArgDouble arg = mArgDoubleCache.get(name.hashCode()); if (arg == null || !arg.getName().equals(name)) { arg = new ArgDouble(name); @@ -494,9 +302,16 @@ public final class PerfettoTrackEventExtra { return this; } - @Override + /** + * Adds a debug arg with key {@code name} and value {@code val}. + */ public Builder addArg(String name, String val) { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } ArgString arg = mArgStringCache.get(name.hashCode()); if (arg == null || !arg.getName().equals(name)) { arg = new ArgString(name); @@ -507,27 +322,79 @@ public final class PerfettoTrackEventExtra { return this; } - @Override + /** + * Adds a flow with {@code id}. + */ public Builder addFlow(int id) { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } Flow flow = mFlowCache.get(sFlowSupplier); flow.setProcessFlow(id); mExtra.addPerfettoPointer(flow); return this; } - @Override + /** + * Adds a terminating flow with {@code id}. + */ public Builder addTerminatingFlow(int id) { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } Flow flow = mFlowCache.get(sFlowSupplier); flow.setProcessTerminatingFlow(id); mExtra.addPerfettoPointer(flow); return this; } - @Override + /** + * Adds a flow with {@code id}. + */ + public Builder setFlow(int id) { + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } + mFlow.setProcessFlow(id); + mExtra.addPerfettoPointer(mFlow); + return this; + } + + /** + * Adds a terminating flow with {@code id}. + */ + public Builder setTerminatingFlow(int id) { + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } + mTerminatingFlow.setProcessTerminatingFlow(id); + mExtra.addPerfettoPointer(mTerminatingFlow); + return this; + } + + /** + * Adds the events to a named track instead of the thread track where the + * event occurred. + */ public Builder usingNamedTrack(long parentUuid, String name) { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } NamedTrack track = mNamedTrackCache.get(name.hashCode()); if (track == null || !track.getName().equals(name)) { @@ -538,19 +405,39 @@ public final class PerfettoTrackEventExtra { return this; } - @Override + /** + * Adds the events to a process scoped named track instead of the thread track where the + * event occurred. + */ public Builder usingProcessNamedTrack(String name) { + if (!mIsCategoryEnabled) { + return this; + } return usingNamedTrack(PerfettoTrace.getProcessTrackUuid(), name); } - @Override + /** + * Adds the events to a thread scoped named track instead of the thread track where the + * event occurred. + */ public Builder usingThreadNamedTrack(long tid, String name) { + if (!mIsCategoryEnabled) { + return this; + } return usingNamedTrack(PerfettoTrace.getThreadTrackUuid(tid), name); } - @Override + /** + * Adds the events to a counter track instead. This is required for + * setting counter values. + */ public Builder usingCounterTrack(long parentUuid, String name) { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } CounterTrack track = mCounterTrackCache.get(name.hashCode()); if (track == null || !track.getName().equals(name)) { @@ -561,95 +448,178 @@ public final class PerfettoTrackEventExtra { return this; } - @Override + /** + * Adds the events to a process scoped counter track instead. This is required for + * setting counter values. + */ public Builder usingProcessCounterTrack(String name) { + if (!mIsCategoryEnabled) { + return this; + } return usingCounterTrack(PerfettoTrace.getProcessTrackUuid(), name); } - @Override + /** + * Adds the events to a thread scoped counter track instead. This is required for + * setting counter values. + */ public Builder usingThreadCounterTrack(long tid, String name) { + if (!mIsCategoryEnabled) { + return this; + } return usingCounterTrack(PerfettoTrace.getThreadTrackUuid(tid), name); } - @Override + /** + * Sets a long counter value on the event. + * + */ public Builder setCounter(long val) { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } mCounterInt64.setValue(val); mExtra.addPerfettoPointer(mCounterInt64); return this; } - @Override + /** + * Sets a double counter value on the event. + * + */ public Builder setCounter(double val) { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } mCounterDouble.setValue(val); mExtra.addPerfettoPointer(mCounterDouble); return this; } - @Override + /** + * Adds a proto field with field id {@code id} and value {@code val}. + */ public Builder addField(long id, long val) { - checkContainer(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkContainer(); + } FieldInt64 field = mFieldInt64Cache.get(sFieldInt64Supplier); field.setValue(id, val); - mCurrentContainer.addField(field); + mExtra.addPerfettoPointer(mCurrentContainer, field); return this; } - @Override + /** + * Adds a proto field with field id {@code id} and value {@code val}. + */ public Builder addField(long id, double val) { - checkContainer(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkContainer(); + } FieldDouble field = mFieldDoubleCache.get(sFieldDoubleSupplier); field.setValue(id, val); - mCurrentContainer.addField(field); + mExtra.addPerfettoPointer(mCurrentContainer, field); return this; } - @Override + /** + * Adds a proto field with field id {@code id} and value {@code val}. + */ public Builder addField(long id, String val) { - checkContainer(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkContainer(); + } FieldString field = mFieldStringCache.get(sFieldStringSupplier); field.setValue(id, val); - mCurrentContainer.addField(field); + mExtra.addPerfettoPointer(mCurrentContainer, field); return this; } - @Override + /** + * Begins a proto field. + * Fields can be added from this point and there must be a corresponding + * {@link endProto}. + * + * The proto field is a singleton and all proto fields get added inside the + * one {@link beginProto} and {@link endProto} within the {@link Builder}. + */ public Builder beginProto() { - checkParent(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkParent(); + } mProto.clearFields(); mExtra.addPerfettoPointer(mProto); - return mBuilderCache.get(sBuilderSupplier).initInternal(this, mProto); + return mBuilderCache.get(sBuilderSupplier).initInternal(this, mProto, true); } - @Override + /** + * Ends a proto field. + */ public Builder endProto() { + if (!mIsCategoryEnabled) { + return this; + } if (mParent == null || mCurrentContainer == null) { throw new IllegalStateException("No proto to end"); } return mParent; } - @Override + /** + * Begins a nested proto field with field id {@code id}. + * Fields can be added from this point and there must be a corresponding + * {@link endNested}. + */ public Builder beginNested(long id) { - checkContainer(); + if (!mIsCategoryEnabled) { + return this; + } + if (DEBUG) { + checkContainer(); + } FieldNested field = mFieldNestedCache.get(sFieldNestedSupplier); field.setId(id); - mCurrentContainer.addField(field); - return mBuilderCache.get(sBuilderSupplier).initInternal(this, field); + mExtra.addPerfettoPointer(mCurrentContainer, field); + return mBuilderCache.get(sBuilderSupplier).initInternal(this, field, true); } - @Override + /** + * Ends a nested proto field. + */ public Builder endNested() { + if (!mIsCategoryEnabled) { + return this; + } if (mParent == null || mCurrentContainer == null) { throw new IllegalStateException("No nested field to end"); } return mParent; } - private Builder initInternal(Builder parent, FieldContainer container) { + + private Builder initInternal(Builder parent, FieldContainer field, + boolean isCategoryEnabled) { mParent = parent; - mCurrentContainer = container; + mCurrentContainer = field; + mIsCategoryEnabled = isCategoryEnabled; mIsBuilt = false; return this; @@ -682,14 +652,8 @@ public final class PerfettoTrackEventExtra { * Start a {@link Builder} to build a {@link PerfettoTrackEventExtra}. */ public static Builder builder() { - return sTrackEventExtra.get().mBuilderCache.get(sBuilderSupplier).initInternal(null, null); - } - - /** - * Returns a no-op {@link Builder}. Useful if a category is disabled. - */ - public static Builder noOpBuilder() { - return NO_OP_BUILDER; + return sTrackEventExtra.get().mBuilderCache.get(sBuilderSupplier).initInternal(null, null, + false); } private final RingBuffer<NamedTrack> mNamedTrackCache = @@ -707,7 +671,7 @@ public final class PerfettoTrackEventExtra { private final Pool<FieldString> mFieldStringCache = new Pool(DEFAULT_EXTRA_CACHE_SIZE); private final Pool<FieldNested> mFieldNestedCache = new Pool(DEFAULT_EXTRA_CACHE_SIZE); private final Pool<Flow> mFlowCache = new Pool(DEFAULT_EXTRA_CACHE_SIZE); - private final Pool<BuilderImpl> mBuilderCache = new Pool(DEFAULT_EXTRA_CACHE_SIZE); + private final Pool<Builder> mBuilderCache = new Pool(DEFAULT_EXTRA_CACHE_SIZE); private static final NativeAllocationRegistry sRegistry = NativeAllocationRegistry.createMalloced( @@ -735,6 +699,15 @@ public final class PerfettoTrackEventExtra { */ public void addPerfettoPointer(PerfettoPointer extra) { native_add_arg(mPtr, extra.getPtr()); + mPendingPointers.add(extra); + } + + /** + * Adds a pointer representing a track event parameter to the {@code container}. + */ + public void addPerfettoPointer(FieldContainer container, PerfettoPointer extra) { + container.addField(extra); + mPendingPointers.add(extra); } /** @@ -742,8 +715,10 @@ public final class PerfettoTrackEventExtra { */ public void reset() { native_clear_args(mPtr); + mPendingPointers.clear(); } + @android.ravenwood.annotation.RavenwoodReplace private CounterInt64 getCounterInt64() { if (mCounterInt64 == null) { mCounterInt64 = new CounterInt64(); @@ -751,6 +726,7 @@ public final class PerfettoTrackEventExtra { return mCounterInt64; } + @android.ravenwood.annotation.RavenwoodReplace private CounterDouble getCounterDouble() { if (mCounterDouble == null) { mCounterDouble = new CounterDouble(); @@ -758,6 +734,7 @@ public final class PerfettoTrackEventExtra { return mCounterDouble; } + @android.ravenwood.annotation.RavenwoodReplace private Proto getProto() { if (mProto == null) { mProto = new Proto(); @@ -765,6 +742,22 @@ public final class PerfettoTrackEventExtra { return mProto; } + @android.ravenwood.annotation.RavenwoodReplace + private Flow getFlow() { + if (mFlow == null) { + mFlow = new Flow(); + } + return mFlow; + } + + @android.ravenwood.annotation.RavenwoodReplace + private Flow getTerminatingFlow() { + if (mTerminatingFlow == null) { + mTerminatingFlow = new Flow(); + } + return mTerminatingFlow; + } + private static final class Flow implements PerfettoPointer { private static final NativeAllocationRegistry sRegistry = NativeAllocationRegistry.createMalloced( @@ -1324,4 +1317,29 @@ public final class PerfettoTrackEventExtra { // Tracing currently completely disabled under Ravenwood return 0; } + + private CounterInt64 getCounterInt64$ravenwood() { + // Tracing currently completely disabled under Ravenwood + return null; + } + + private CounterDouble getCounterDouble$ravenwood() { + // Tracing currently completely disabled under Ravenwood + return null; + } + + private Proto getProto$ravenwood() { + // Tracing currently completely disabled under Ravenwood + return null; + } + + private Flow getFlow$ravenwood() { + // Tracing currently completely disabled under Ravenwood + return null; + } + + private Flow getTerminatingFlow$ravenwood() { + // Tracing currently completely disabled under Ravenwood + return null; + } } diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java deleted file mode 100644 index 435c34f832c6..000000000000 --- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java +++ /dev/null @@ -1,1600 +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 android.os; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.TestApi; -import android.ravenwood.annotation.RavenwoodKeepWholeClass; -import android.ravenwood.annotation.RavenwoodRedirect; -import android.ravenwood.annotation.RavenwoodRedirectionClass; -import android.util.Log; -import android.util.Printer; -import android.util.SparseArray; -import android.util.proto.ProtoOutputStream; - -import com.android.internal.annotations.GuardedBy; - -import dalvik.annotation.optimization.NeverCompile; - -import java.io.FileDescriptor; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.PriorityQueue; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Low-level class holding the list of messages to be dispatched by a - * {@link Looper}. Messages are not added directly to a MessageQueue, - * but rather through {@link Handler} objects associated with the Looper. - * - * <p>You can retrieve the MessageQueue for the current thread with - * {@link Looper#myQueue() Looper.myQueue()}. - */ -@RavenwoodKeepWholeClass -@RavenwoodRedirectionClass("MessageQueue_ravenwood") -public final class MessageQueue { - private static final String TAG = "SemiConcurrentMessageQueue"; - private static final boolean DEBUG = false; - private static final boolean TRACE = false; - - // True if the message queue can be quit. - private final boolean mQuitAllowed; - - @SuppressWarnings("unused") - private long mPtr; // used by native code - - @IntDef(value = { - STACK_NODE_MESSAGE, - STACK_NODE_ACTIVE, - STACK_NODE_PARKED, - STACK_NODE_TIMEDPARK}) - @Retention(RetentionPolicy.SOURCE) - private @interface StackNodeType {} - - /* - * Stack node types. STACK_NODE_MESSAGE indicates a node containing a message. - * The other types indicate what state our Looper thread is in. The bottom of - * the stack is always a single state node. Message nodes are added on top. - */ - private static final int STACK_NODE_MESSAGE = 0; - /* - * Active state indicates that next() is processing messages - */ - private static final int STACK_NODE_ACTIVE = 1; - /* - * Parked state indicates that the Looper thread is sleeping indefinitely (nothing to deliver) - */ - private static final int STACK_NODE_PARKED = 2; - /* - * Timed Park state indicates that the Looper thread is sleeping, waiting for a message - * deadline - */ - private static final int STACK_NODE_TIMEDPARK = 3; - - /* Describes a node in the Treiber stack */ - static class StackNode { - @StackNodeType - private final int mType; - - StackNode(@StackNodeType int type) { - mType = type; - } - - @StackNodeType - final int getNodeType() { - return mType; - } - - final boolean isMessageNode() { - return mType == STACK_NODE_MESSAGE; - } - } - - static final class MessageNode extends StackNode implements Comparable<MessageNode> { - private final Message mMessage; - volatile StackNode mNext; - StateNode mBottomOfStack; - boolean mWokeUp; - boolean mRemovedFromStack = false; - final long mInsertSeq; - - MessageNode(@NonNull Message message, long insertSeq) { - super(STACK_NODE_MESSAGE); - mMessage = message; - mInsertSeq = insertSeq; - } - - long getWhen() { - return mMessage.when; - } - - boolean isRemovedFromStack() { - return mRemovedFromStack; - } - - boolean removeFromStack() { - if (!mRemovedFromStack) { - mRemovedFromStack = true; - return true; - } - return false; - } - - boolean isAsync() { - return mMessage.isAsynchronous(); - } - - boolean isBarrier() { - return mMessage.target == null; - } - - @Override - public int compareTo(@NonNull MessageNode messageNode) { - Message other = messageNode.mMessage; - - int compared = Long.compare(mMessage.when, other.when); - if (compared == 0) { - compared = Long.compare(mInsertSeq, messageNode.mInsertSeq); - } - return compared; - } - } - - static class StateNode extends StackNode { - StateNode(int type) { - super(type); - } - } - - static final class TimedParkStateNode extends StateNode { - long mWhenToWake; - - TimedParkStateNode() { - super(STACK_NODE_TIMEDPARK); - } - } - - private static final StateNode sStackStateActive = new StateNode(STACK_NODE_ACTIVE); - private static final StateNode sStackStateParked = new StateNode(STACK_NODE_PARKED); - private final TimedParkStateNode mStackStateTimedPark = new TimedParkStateNode(); - - /* This is the top of our treiber stack. */ - private static final VarHandle sState; - static { - try { - MethodHandles.Lookup l = MethodHandles.lookup(); - sState = l.findVarHandle(MessageQueue.class, "mStateValue", - MessageQueue.StackNode.class); - } catch (Exception e) { - Log.wtf(TAG, "VarHandle lookup failed with exception: " + e); - throw new ExceptionInInitializerError(e); - } - } - - private volatile StackNode mStateValue = sStackStateParked; - @GuardedBy("mPriorityQueue") - private final PriorityQueue<MessageNode> mPriorityQueue = - new PriorityQueue<MessageNode>(); - @GuardedBy("mPriorityQueue") - private final PriorityQueue<MessageNode> mAsyncPriorityQueue = - new PriorityQueue<MessageNode>(); - - /* - * This helps us ensure that messages with the same timestamp are inserted in FIFO order. - * Increments on each insert, starting at 0. MessageNode.compareTo() will compare sequences - * when delivery timestamps are identical. - */ - private static final VarHandle sNextInsertSeq; - private volatile long mNextInsertSeqValue = 0; - /* - * The exception to the FIFO order rule is sendMessageAtFrontOfQueue(). - * Those messages must be in LIFO order. - * Decrements on each front of queue insert. - */ - private static final VarHandle sNextFrontInsertSeq; - private volatile long mNextFrontInsertSeqValue = -1; - static { - try { - MethodHandles.Lookup l = MethodHandles.lookup(); - sNextInsertSeq = l.findVarHandle(MessageQueue.class, "mNextInsertSeqValue", - long.class); - sNextFrontInsertSeq = l.findVarHandle(MessageQueue.class, "mNextFrontInsertSeqValue", - long.class); - } catch (Exception e) { - Log.wtf(TAG, "VarHandle lookup failed with exception: " + e); - throw new ExceptionInInitializerError(e); - } - - } - - /* - * Tracks the number of queued and cancelled messages in our stack. - * - * On item cancellation, determine whether to wake next() to flush tombstoned messages. - * We track queued and cancelled counts as two ints packed into a single long. - */ - private static final class MessageCounts { - private static VarHandle sCounts; - private volatile long mCountsValue = 0; - static { - try { - MethodHandles.Lookup l = MethodHandles.lookup(); - sCounts = l.findVarHandle(MessageQueue.MessageCounts.class, "mCountsValue", - long.class); - } catch (Exception e) { - Log.wtf(TAG, "VarHandle lookup failed with exception: " + e); - throw new ExceptionInInitializerError(e); - } - } - /* We use a special value to indicate when next() has been woken for flush. */ - private static final long AWAKE = Long.MAX_VALUE; - /* - * Minimum number of messages in the stack which we need before we consider flushing - * tombstoned items. - */ - private static final int MESSAGE_FLUSH_THRESHOLD = 10; - - private static int numQueued(long val) { - return (int) (val >>> Integer.SIZE); - } - - private static int numCancelled(long val) { - return (int) val; - } - - private static long combineCounts(int queued, int cancelled) { - return ((long) queued << Integer.SIZE) | (long) cancelled; - } - - public void incrementQueued() { - while (true) { - long oldVal = mCountsValue; - int queued = numQueued(oldVal); - int cancelled = numCancelled(oldVal); - /* Use Math.max() to avoid overflow of queued count */ - long newVal = combineCounts(Math.max(queued + 1, queued), cancelled); - - /* Don't overwrite 'AWAKE' state */ - if (oldVal == AWAKE || sCounts.compareAndSet(this, oldVal, newVal)) { - break; - } - } - } - - public boolean incrementCancelled() { - while (true) { - long oldVal = mCountsValue; - if (oldVal == AWAKE) { - return false; - } - int queued = numQueued(oldVal); - int cancelled = numCancelled(oldVal); - boolean needsPurge = queued > MESSAGE_FLUSH_THRESHOLD - && (queued >> 1) < cancelled; - long newVal; - if (needsPurge) { - newVal = AWAKE; - } else { - newVal = combineCounts(queued, - Math.max(cancelled + 1, cancelled)); - } - - if (sCounts.compareAndSet(this, oldVal, newVal)) { - return needsPurge; - } - } - } - - public void clearCounts() { - mCountsValue = 0; - } - } - - private final MessageCounts mMessageCounts = new MessageCounts(); - - private final Object mIdleHandlersLock = new Object(); - @GuardedBy("mIdleHandlersLock") - private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>(); - private IdleHandler[] mPendingIdleHandlers; - - private final Object mFileDescriptorRecordsLock = new Object(); - @GuardedBy("mFileDescriptorRecordsLock") - private SparseArray<FileDescriptorRecord> mFileDescriptorRecords; - - private static final VarHandle sQuitting; - private boolean mQuittingValue = false; - static { - try { - MethodHandles.Lookup l = MethodHandles.lookup(); - sQuitting = l.findVarHandle(MessageQueue.class, "mQuittingValue", boolean.class); - } catch (Exception e) { - Log.wtf(TAG, "VarHandle lookup failed with exception: " + e); - throw new ExceptionInInitializerError(e); - } - } - - // The next barrier token. - // Barriers are indicated by messages with a null target whose arg1 field carries the token. - private final AtomicInteger mNextBarrierToken = new AtomicInteger(1); - - @RavenwoodRedirect - private static native long nativeInit(); - @RavenwoodRedirect - private static native void nativeDestroy(long ptr); - @RavenwoodRedirect - private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ - @RavenwoodRedirect - private static native void nativeWake(long ptr); - @RavenwoodRedirect - private static native boolean nativeIsPolling(long ptr); - @RavenwoodRedirect - private static native void nativeSetFileDescriptorEvents(long ptr, int fd, int events); - - MessageQueue(boolean quitAllowed) { - mQuitAllowed = quitAllowed; - mPtr = nativeInit(); - } - - @Override - protected void finalize() throws Throwable { - try { - dispose(); - } finally { - super.finalize(); - } - } - - // Disposes of the underlying message queue. - // Must only be called on the looper thread or the finalizer. - private void dispose() { - if (mPtr != 0) { - nativeDestroy(mPtr); - mPtr = 0; - } - } - - /** - * Returns true if the looper has no pending messages which are due to be processed. - * - * <p>This method is safe to call from any thread. - * - * @return True if the looper is idle. - */ - public boolean isIdle() { - MessageNode msgNode = null; - MessageNode asyncMsgNode = null; - - synchronized (mPriorityQueue) { - msgNode = mPriorityQueue.peek(); - asyncMsgNode = mAsyncPriorityQueue.peek(); - - final long now = SystemClock.uptimeMillis(); - if ((msgNode != null && msgNode.getWhen() <= now) - || (asyncMsgNode != null && asyncMsgNode.getWhen() <= now)) { - return false; - } - } - - return true; - } - - /** - * Add a new {@link IdleHandler} to this message queue. This may be - * removed automatically for you by returning false from - * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is - * invoked, or explicitly removing it with {@link #removeIdleHandler}. - * - * <p>This method is safe to call from any thread. - * - * @param handler The IdleHandler to be added. - */ - public void addIdleHandler(@NonNull IdleHandler handler) { - if (handler == null) { - throw new NullPointerException("Can't add a null IdleHandler"); - } - synchronized (mIdleHandlersLock) { - mIdleHandlers.add(handler); - } - } - - /** - * Remove an {@link IdleHandler} from the queue that was previously added - * with {@link #addIdleHandler}. If the given object is not currently - * in the idle list, nothing is done. - * - * <p>This method is safe to call from any thread. - * - * @param handler The IdleHandler to be removed. - */ - public void removeIdleHandler(@NonNull IdleHandler handler) { - synchronized (mIdleHandlersLock) { - mIdleHandlers.remove(handler); - } - } - - /** - * Returns whether this looper's thread is currently polling for more work to do. - * This is a good signal that the loop is still alive rather than being stuck - * handling a callback. Note that this method is intrinsically racy, since the - * state of the loop can change before you get the result back. - * - * <p>This method is safe to call from any thread. - * - * @return True if the looper is currently polling for events. - * @hide - */ - public boolean isPolling() { - // If the loop is quitting then it must not be idling. - // We can assume mPtr != 0 when sQuitting is false. - return !((boolean) sQuitting.getVolatile(this)) && nativeIsPolling(mPtr); - } - - /* Helper to choose the correct queue to insert into. */ - @GuardedBy("mPriorityQueue") - private void insertIntoPriorityQueue(MessageNode msgNode) { - if (msgNode.isAsync()) { - mAsyncPriorityQueue.offer(msgNode); - } else { - mPriorityQueue.offer(msgNode); - } - } - - @GuardedBy("mPriorityQueue") - private boolean removeFromPriorityQueue(MessageNode msgNode) { - if (msgNode.isAsync()) { - return mAsyncPriorityQueue.remove(msgNode); - } else { - return mPriorityQueue.remove(msgNode); - } - } - - private MessageNode pickEarliestNode(MessageNode nodeA, MessageNode nodeB) { - if (nodeA != null && nodeB != null) { - if (nodeA.compareTo(nodeB) < 0) { - return nodeA; - } - return nodeB; - } - - return nodeA != null ? nodeA : nodeB; - } - - /* Move any non-cancelled messages into the priority queue */ - private void drainStack(StackNode oldTop) { - while (oldTop.isMessageNode()) { - MessageNode oldTopMessageNode = (MessageNode) oldTop; - if (oldTopMessageNode.removeFromStack()) { - insertIntoPriorityQueue(oldTopMessageNode); - } - MessageNode inserted = oldTopMessageNode; - oldTop = oldTopMessageNode.mNext; - } - } - - /* Set the stack state to Active, return a list of nodes to walk. */ - private StackNode swapAndSetStackStateActive() { - while (true) { - /* Set stack state to Active, get node list to walk later */ - StackNode current = (StackNode) sState.getVolatile(this); - if (current == sStackStateActive - || sState.compareAndSet(this, current, sStackStateActive)) { - return current; - } - } - } - - /* This is only read/written from the Looper thread */ - private int mNextPollTimeoutMillis; - private static final AtomicLong mMessagesDelivered = new AtomicLong(); - - private Message nextMessage() { - int i = 0; - - while (true) { - if (DEBUG) { - Log.d(TAG, "nextMessage loop #" + i); - i++; - } - - /* This protects us from racing with remove. Enqueue can still add items. */ - synchronized (mPriorityQueue) { - - /* - * Set our state to active, drain any items from the stack into our priority queues - */ - StackNode oldTop; - oldTop = swapAndSetStackStateActive(); - drainStack(oldTop); - - /* - * The objective of this next block of code is to: - * - find a message to return (if any is ready) - * - find a next message we would like to return, after scheduling. - * - we make our scheduling decision based on this next message (if it exists). - * - * We have two queues to juggle and the presence of barriers throws an additional - * wrench into our plans. - */ - - /* Get the first node from each queue */ - MessageNode msgNode = mPriorityQueue.peek(); - MessageNode asyncMsgNode = mAsyncPriorityQueue.peek(); - - if (DEBUG) { - if (msgNode != null) { - Message msg = msgNode.mMessage; - Log.d(TAG, "Next found node what: " + msg.what + " when: " + msg.when - + " seq: " + msgNode.mInsertSeq + "barrier: " - + msgNode.isBarrier() + " now: " - + SystemClock.uptimeMillis()); - } - if (asyncMsgNode != null) { - Message msg = asyncMsgNode.mMessage; - Log.d(TAG, "Next found async node what: " + msg.what + " when: " + msg.when - + " seq: " + asyncMsgNode.mInsertSeq + "barrier: " - + asyncMsgNode.isBarrier() + " now: " - + SystemClock.uptimeMillis()); - } - } - - /* - * the node which we will return, null if none are ready - */ - MessageNode found = null; - /* - * The node from which we will determine our next wakeup time. - * Null indicates there is no next message ready. If we found a node, - * we can leave this null as Looper will call us again after delivering - * the message. - */ - MessageNode next = null; - - long now = SystemClock.uptimeMillis(); - /* - * If we have a barrier we should return the async node if it exists and is - * ready - */ - if (msgNode != null && msgNode.isBarrier()) { - if (asyncMsgNode != null && now >= asyncMsgNode.getWhen()) { - found = asyncMsgNode; - removeFromPriorityQueue(found); - } else { - next = asyncMsgNode; - } - } else { /* No barrier. */ - MessageNode earliest; - /* - * If we have two messages, pick the earliest option from either queue. - * Otherwise grab whichever node is non-null. If both are null we'll fall - * through. - */ - earliest = pickEarliestNode(msgNode, asyncMsgNode); - - if (earliest != null) { - if (now >= earliest.getWhen()) { - found = earliest; - removeFromPriorityQueue(found); - } else { - next = earliest; - } - } - } - - if (DEBUG) { - if (found != null) { - Message msg = found.mMessage; - Log.d(TAG, "Will deliver node what: " + msg.what + " when: " + msg.when - + " seq: " + found.mInsertSeq + " barrier: " - + found.isBarrier() + " async: " + found.isAsync() - + " now: " + SystemClock.uptimeMillis()); - } else { - Log.d(TAG, "No node to deliver"); - } - if (next != null) { - Message msg = next.mMessage; - Log.d(TAG, "Next node what: " + msg.what + " when: " + msg.when + " seq: " - + next.mInsertSeq + " barrier: " + next.isBarrier() - + " async: " + next.isAsync() - + " now: " + SystemClock.uptimeMillis()); - } else { - Log.d(TAG, "No next node"); - } - } - - /* - * If we have a found message, we will get called again so there's no need to set - * state. - * In that case we can leave our state as ACTIVE. - * - * Otherwise we should determine how to park the thread. - */ - StateNode nextOp = sStackStateActive; - if (found == null) { - if (next == null) { - /* No message to deliver, sleep indefinitely */ - mNextPollTimeoutMillis = -1; - nextOp = sStackStateParked; - if (DEBUG) { - Log.d(TAG, "nextMessage next state is StackStateParked"); - } - } else { - /* Message not ready, or we found one to deliver already, set a timeout */ - long nextMessageWhen = next.getWhen(); - if (nextMessageWhen > now) { - mNextPollTimeoutMillis = (int) Math.min(nextMessageWhen - now, - Integer.MAX_VALUE); - } else { - mNextPollTimeoutMillis = 0; - } - - mStackStateTimedPark.mWhenToWake = now + mNextPollTimeoutMillis; - nextOp = mStackStateTimedPark; - if (DEBUG) { - Log.d(TAG, "nextMessage next state is StackStateTimedParked " - + " next timeout ms " + mNextPollTimeoutMillis - + " mWhenToWake: " + mStackStateTimedPark.mWhenToWake - + " now " + now); - } - } - } - - /* - * Try to swap our state from Active back to Park or TimedPark. If we raced with - * enqueue, loop back around to pick up any new items. - */ - if (sState.compareAndSet(this, sStackStateActive, nextOp)) { - mMessageCounts.clearCounts(); - if (found != null) { - if (TRACE) { - Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet()); - } - return found.mMessage; - } - return null; - } - if (found != null) { - /* - * Add this node back - we will be adding new nodes into our priority queue, and - * recalculating what to return. - */ - insertIntoPriorityQueue(found); - } - } - } - } - - Message next() { - final long ptr = mPtr; - if (ptr == 0) { - return null; - } - - mNextPollTimeoutMillis = 0; - int pendingIdleHandlerCount = -1; // -1 only during first iteration - while (true) { - if (mNextPollTimeoutMillis != 0) { - Binder.flushPendingCommands(); - } - - nativePollOnce(ptr, mNextPollTimeoutMillis); - - Message msg = nextMessage(); - if (msg != null) { - msg.markInUse(); - return msg; - } - - if ((boolean) sQuitting.getVolatile(this)) { - return null; - } - - synchronized (mIdleHandlersLock) { - // If first time idle, then get the number of idlers to run. - // Idle handles only run if the queue is empty or if the first message - // in the queue (possibly a barrier) is due to be handled in the future. - if (pendingIdleHandlerCount < 0 - && isIdle()) { - pendingIdleHandlerCount = mIdleHandlers.size(); - } - if (pendingIdleHandlerCount <= 0) { - // No idle handlers to run. Loop and wait some more. - continue; - } - - if (mPendingIdleHandlers == null) { - mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; - } - mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); - } - - // Run the idle handlers. - // We only ever reach this code block during the first iteration. - for (int i = 0; i < pendingIdleHandlerCount; i++) { - final IdleHandler idler = mPendingIdleHandlers[i]; - mPendingIdleHandlers[i] = null; // release the reference to the handler - - boolean keep = false; - try { - keep = idler.queueIdle(); - } catch (Throwable t) { - Log.wtf(TAG, "IdleHandler threw exception", t); - } - - if (!keep) { - synchronized (mIdleHandlersLock) { - mIdleHandlers.remove(idler); - } - } - } - - // Reset the idle handler count to 0 so we do not run them again. - pendingIdleHandlerCount = 0; - - // While calling an idle handler, a new message could have been delivered - // so go back and look again for a pending message without waiting. - mNextPollTimeoutMillis = 0; - } - } - - void quit(boolean safe) { - if (!mQuitAllowed) { - throw new IllegalStateException("Main thread not allowed to quit."); - } - synchronized (mIdleHandlersLock) { - if (sQuitting.compareAndSet(this, false, true)) { - if (safe) { - removeAllFutureMessages(); - } else { - removeAllMessages(); - } - - // We can assume mPtr != 0 because sQuitting was previously false. - nativeWake(mPtr); - } - } - } - - boolean enqueueMessage(@NonNull Message msg, long when) { - if (msg.target == null) { - throw new IllegalArgumentException("Message must have a target."); - } - - if (msg.isInUse()) { - throw new IllegalStateException(msg + " This message is already in use."); - } - - return enqueueMessageUnchecked(msg, when); - } - - private boolean enqueueMessageUnchecked(@NonNull Message msg, long when) { - if ((boolean) sQuitting.getVolatile(this)) { - IllegalStateException e = new IllegalStateException( - msg.target + " sending message to a Handler on a dead thread"); - Log.w(TAG, e.getMessage(), e); - msg.recycleUnchecked(); - return false; - } - - long seq = when != 0 ? ((long)sNextInsertSeq.getAndAdd(this, 1L) + 1L) - : ((long)sNextFrontInsertSeq.getAndAdd(this, -1L) - 1L); - /* TODO: Add a MessageNode member to Message so we can avoid this allocation */ - MessageNode node = new MessageNode(msg, seq); - msg.when = when; - msg.markInUse(); - - if (DEBUG) { - Log.d(TAG, "Insert message what: " + msg.what + " when: " + msg.when + " seq: " - + node.mInsertSeq + " barrier: " + node.isBarrier() + " async: " - + node.isAsync() + " now: " + SystemClock.uptimeMillis()); - } - - while (true) { - StackNode old = (StackNode) sState.getVolatile(this); - boolean wakeNeeded; - boolean inactive; - - node.mNext = old; - switch (old.getNodeType()) { - case STACK_NODE_ACTIVE: - /* - * The worker thread is currently active and will process any elements added to - * the stack before parking again. - */ - node.mBottomOfStack = (StateNode) old; - inactive = false; - node.mWokeUp = true; - wakeNeeded = false; - break; - - case STACK_NODE_PARKED: - node.mBottomOfStack = (StateNode) old; - inactive = true; - node.mWokeUp = true; - wakeNeeded = true; - break; - - case STACK_NODE_TIMEDPARK: - node.mBottomOfStack = (StateNode) old; - inactive = true; - wakeNeeded = mStackStateTimedPark.mWhenToWake >= node.getWhen(); - node.mWokeUp = wakeNeeded; - break; - - default: - MessageNode oldMessage = (MessageNode) old; - - node.mBottomOfStack = oldMessage.mBottomOfStack; - int bottomType = node.mBottomOfStack.getNodeType(); - inactive = bottomType >= STACK_NODE_PARKED; - wakeNeeded = (bottomType == STACK_NODE_TIMEDPARK - && mStackStateTimedPark.mWhenToWake >= node.getWhen() - && !oldMessage.mWokeUp); - node.mWokeUp = oldMessage.mWokeUp || wakeNeeded; - break; - } - if (sState.compareAndSet(this, old, node)) { - if (inactive) { - if (wakeNeeded) { - nativeWake(mPtr); - } else { - mMessageCounts.incrementQueued(); - } - } - return true; - } - } - } - - /** - * Posts a synchronization barrier to the Looper's message queue. - * - * Message processing occurs as usual until the message queue encounters the - * synchronization barrier that has been posted. When the barrier is encountered, - * later synchronous messages in the queue are stalled (prevented from being executed) - * until the barrier is released by calling {@link #removeSyncBarrier} and specifying - * the token that identifies the synchronization barrier. - * - * This method is used to immediately postpone execution of all subsequently posted - * synchronous messages until a condition is met that releases the barrier. - * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier - * and continue to be processed as usual. - * - * This call must be always matched by a call to {@link #removeSyncBarrier} with - * the same token to ensure that the message queue resumes normal operation. - * Otherwise the application will probably hang! - * - * @return A token that uniquely identifies the barrier. This token must be - * passed to {@link #removeSyncBarrier} to release the barrier. - * - * @hide - */ - @TestApi - public int postSyncBarrier() { - return postSyncBarrier(SystemClock.uptimeMillis()); - } - - private int postSyncBarrier(long when) { - final int token = mNextBarrierToken.getAndIncrement(); - final Message msg = Message.obtain(); - - msg.markInUse(); - msg.arg1 = token; - - if (!enqueueMessageUnchecked(msg, when)) { - Log.wtf(TAG, "Unexpected error while adding sync barrier!"); - return -1; - } - - return token; - } - - private class MatchBarrierToken extends MessageCompare { - int mBarrierToken; - - MatchBarrierToken(int token) { - super(); - mBarrierToken = token; - } - - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == null && m.arg1 == mBarrierToken) { - return true; - } - return false; - } - } - - /** - * Removes a synchronization barrier. - * - * @param token The synchronization barrier token that was returned by - * {@link #postSyncBarrier}. - * - * @throws IllegalStateException if the barrier was not found. - * - * @hide - */ - @TestApi - public void removeSyncBarrier(int token) { - boolean removed; - MessageNode first; - final MatchBarrierToken matchBarrierToken = new MatchBarrierToken(token); - - synchronized (mPriorityQueue) { - try { - /* Retain the first element to see if we are currently stuck on a barrier. */ - first = mPriorityQueue.peek(); - } catch (NoSuchElementException e) { - /* The queue is empty */ - first = null; - } - - removed = findOrRemoveMessages(null, 0, null, null, 0, matchBarrierToken, true); - if (removed && first != null) { - Message m = first.mMessage; - if (m.target == null && m.arg1 == token) { - /* Wake up next() in case it was sleeping on this barrier. */ - nativeWake(mPtr); - } - } else if (!removed) { - throw new IllegalStateException("The specified message queue synchronization " - + " barrier token has not been posted or has already been removed."); - } - } - } - - private StateNode getStateNode(StackNode node) { - if (node.isMessageNode()) { - return ((MessageNode) node).mBottomOfStack; - } - return (StateNode) node; - } - - /* - * This class is used to find matches for hasMessages() and removeMessages() - */ - private abstract static class MessageCompare { - public abstract boolean compareMessage(Message m, Handler h, int what, Object object, - Runnable r, long when); - } - @GuardedBy("mPriorityQueue") - private boolean stackHasMessages(Handler h, int what, Object object, Runnable r, long when, - MessageCompare compare, boolean removeMatches) { - boolean found = false; - StackNode top = (StackNode) sState.getVolatile(this); - StateNode bottom = getStateNode(top); - - /* No messages to search. */ - if (!top.isMessageNode()) { - return false; - } - - /* - * We have messages that we may tombstone. Walk the stack until we hit the bottom. - * next() will remove them on it's next pass. - */ - if (!(top instanceof MessageNode)) { - Log.wtf(TAG, "Unknown node type found in Trieber stack"); - } - MessageNode p = (MessageNode) top; - - while (true) { - if (compare.compareMessage(p.mMessage, h, what, object, r, when)) { - found = true; - if (DEBUG) { - Log.w(TAG, "stackHasMessages node matches"); - } - if (removeMatches) { - if (p.removeFromStack()) { - p.mMessage.recycleUnchecked(); - if (mMessageCounts.incrementCancelled()) { - nativeWake(mPtr); - } - } - } else { - return true; - } - } - - StackNode n = p.mNext; - if (!n.isMessageNode()) { - /* We reached the end of the stack */ - return found; - } - p = (MessageNode) n; - } - } - - @GuardedBy("mPriorityQueue") - private boolean priorityQueueHasMessage(PriorityQueue queue, Handler h, - int what, Object object, Runnable r, long when, MessageCompare compare, - boolean removeMatches) { - Iterator<MessageNode> iterator = queue.iterator(); - boolean found = false; - - while (iterator.hasNext()) { - MessageNode msg = iterator.next(); - - if (compare.compareMessage(msg.mMessage, h, what, object, r, when)) { - if (removeMatches) { - found = true; - iterator.remove(); - msg.mMessage.recycleUnchecked(); - } else { - return true; - } - } - } - return found; - } - - private boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when, - MessageCompare compare, boolean removeMatches) { - boolean foundInStack, foundInQueue; - - synchronized (mPriorityQueue) { - foundInStack = stackHasMessages(h, what, object, r, when, compare, removeMatches); - foundInQueue = priorityQueueHasMessage(mPriorityQueue, h, what, object, r, when, - compare, removeMatches); - foundInQueue |= priorityQueueHasMessage(mAsyncPriorityQueue, h, what, object, r, when, - compare, removeMatches); - - return foundInStack || foundInQueue; - } - } - - private static class MatchHandlerWhatAndObject extends MessageCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && m.what == what && (object == null || m.obj == object)) { - return true; - } - return false; - } - } - private final MatchHandlerWhatAndObject mMatchHandlerWhatAndObject = - new MatchHandlerWhatAndObject(); - boolean hasMessages(Handler h, int what, Object object) { - if (h == null) { - return false; - } - - return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, false); - } - - private static class MatchHandlerWhatAndObjectEquals extends MessageCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && m.what == what && (object == null || object.equals(m.obj))) { - return true; - } - return false; - } - } - private final MatchHandlerWhatAndObjectEquals mMatchHandlerWhatAndObjectEquals = - new MatchHandlerWhatAndObjectEquals(); - boolean hasEqualMessages(Handler h, int what, Object object) { - if (h == null) { - return false; - } - - return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals, - false); - } - - private static class MatchHandlerRunnableAndObject extends MessageCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && m.callback == r && (object == null || m.obj == object)) { - return true; - } - return false; - } - } - private final MatchHandlerRunnableAndObject mMatchHandlerRunnableAndObject = - new MatchHandlerRunnableAndObject(); - - boolean hasMessages(Handler h, Runnable r, Object object) { - if (h == null) { - return false; - } - - return findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, false); - } - - private static class MatchHandler extends MessageCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h) { - return true; - } - return false; - } - } - private final MatchHandler mMatchHandler = new MatchHandler(); - boolean hasMessages(Handler h) { - if (h == null) { - return false; - } - return findOrRemoveMessages(h, -1, null, null, 0, mMatchHandler, false); - } - - void removeMessages(Handler h, int what, Object object) { - if (h == null) { - return; - } - findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, true); - } - - void removeEqualMessages(Handler h, int what, Object object) { - if (h == null) { - return; - } - findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals, true); - } - - void removeMessages(Handler h, Runnable r, Object object) { - if (h == null || r == null) { - return; - } - findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, true); - } - - private static class MatchHandlerRunnableAndObjectEquals extends MessageCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && m.callback == r && (object == null || object.equals(m.obj))) { - return true; - } - return false; - } - } - private final MatchHandlerRunnableAndObjectEquals mMatchHandlerRunnableAndObjectEquals = - new MatchHandlerRunnableAndObjectEquals(); - void removeEqualMessages(Handler h, Runnable r, Object object) { - if (h == null || r == null) { - return; - } - findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObjectEquals, true); - } - - private static class MatchHandlerAndObject extends MessageCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && (object == null || m.obj == object)) { - return true; - } - return false; - } - } - private final MatchHandlerAndObject mMatchHandlerAndObject = new MatchHandlerAndObject(); - void removeCallbacksAndMessages(Handler h, Object object) { - if (h == null) { - return; - } - findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObject, true); - } - - private static class MatchHandlerAndObjectEquals extends MessageCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.target == h && (object == null || object.equals(m.obj))) { - return true; - } - return false; - } - } - private final MatchHandlerAndObjectEquals mMatchHandlerAndObjectEquals = - new MatchHandlerAndObjectEquals(); - void removeCallbacksAndEqualMessages(Handler h, Object object) { - if (h == null) { - return; - } - findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObjectEquals, true); - } - - private static class MatchAllMessages extends MessageCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - return true; - } - } - private final MatchAllMessages mMatchAllMessages = new MatchAllMessages(); - private void removeAllMessages() { - findOrRemoveMessages(null, -1, null, null, 0, mMatchAllMessages, true); - } - - private static class MatchAllFutureMessages extends MessageCompare { - @Override - public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, - long when) { - if (m.when > when) { - return true; - } - return false; - } - } - private final MatchAllFutureMessages mMatchAllFutureMessages = new MatchAllFutureMessages(); - private void removeAllFutureMessages() { - findOrRemoveMessages(null, -1, null, null, SystemClock.uptimeMillis(), - mMatchAllFutureMessages, true); - } - - @NeverCompile - private void printPriorityQueueNodes() { - Iterator<MessageNode> iterator = mPriorityQueue.iterator(); - - Log.d(TAG, "* Dump priority queue"); - while (iterator.hasNext()) { - MessageNode msgNode = iterator.next(); - Log.d(TAG, "** MessageNode what: " + msgNode.mMessage.what + " when " - + msgNode.mMessage.when + " seq: " + msgNode.mInsertSeq); - } - } - - @NeverCompile - private int dumpPriorityQueue(PriorityQueue<MessageNode> queue, Printer pw, String prefix, - Handler h, int n) { - int count = 0; - long now = SystemClock.uptimeMillis(); - - for (MessageNode msgNode : queue) { - Message msg = msgNode.mMessage; - if (h == null || h == msg.target) { - pw.println(prefix + "Message " + (n + count) + ": " + msg.toString(now)); - } - count++; - } - return count; - } - - @NeverCompile - void dump(Printer pw, String prefix, Handler h) { - long now = SystemClock.uptimeMillis(); - int n = 0; - - pw.println(prefix + "(MessageQueue is using SemiConcurrent implementation)"); - - StackNode node = (StackNode) sState.getVolatile(this); - while (node != null) { - if (node.isMessageNode()) { - Message msg = ((MessageNode) node).mMessage; - if (h == null || h == msg.target) { - pw.println(prefix + "Message " + n + ": " + msg.toString(now)); - } - node = ((MessageNode) node).mNext; - } else { - pw.println(prefix + "State: " + node); - node = null; - } - n++; - } - - synchronized (mPriorityQueue) { - pw.println(prefix + "PriorityQueue Messages: "); - n += dumpPriorityQueue(mPriorityQueue, pw, prefix, h, n); - pw.println(prefix + "AsyncPriorityQueue Messages: "); - n += dumpPriorityQueue(mAsyncPriorityQueue, pw, prefix, h, n); - } - - pw.println(prefix + "(Total messages: " + n + ", polling=" + isPolling() - + ", quitting=" + (boolean) sQuitting.getVolatile(this) + ")"); - } - - @NeverCompile - private int dumpPriorityQueue(PriorityQueue<MessageNode> queue, ProtoOutputStream proto) { - int count = 0; - - for (MessageNode msgNode : queue) { - Message msg = msgNode.mMessage; - msg.dumpDebug(proto, MessageQueueProto.MESSAGES); - count++; - } - return count; - } - - @NeverCompile - void dumpDebug(ProtoOutputStream proto, long fieldId) { - final long messageQueueToken = proto.start(fieldId); - - StackNode node = (StackNode) sState.getVolatile(this); - while (node.isMessageNode()) { - Message msg = ((MessageNode) node).mMessage; - msg.dumpDebug(proto, MessageQueueProto.MESSAGES); - node = ((MessageNode) node).mNext; - } - - synchronized (mPriorityQueue) { - dumpPriorityQueue(mPriorityQueue, proto); - dumpPriorityQueue(mAsyncPriorityQueue, proto); - } - - proto.write(MessageQueueProto.IS_POLLING_LOCKED, isPolling()); - proto.write(MessageQueueProto.IS_QUITTING, (boolean) sQuitting.getVolatile(this)); - proto.end(messageQueueToken); - } - - /** - * Adds a file descriptor listener to receive notification when file descriptor - * related events occur. - * <p> - * If the file descriptor has already been registered, the specified events - * and listener will replace any that were previously associated with it. - * It is not possible to set more than one listener per file descriptor. - * </p><p> - * It is important to always unregister the listener when the file descriptor - * is no longer of use. - * </p> - * - * @param fd The file descriptor for which a listener will be registered. - * @param events The set of events to receive: a combination of the - * {@link OnFileDescriptorEventListener#EVENT_INPUT}, - * {@link OnFileDescriptorEventListener#EVENT_OUTPUT}, and - * {@link OnFileDescriptorEventListener#EVENT_ERROR} event masks. If the requested - * set of events is zero, then the listener is unregistered. - * @param listener The listener to invoke when file descriptor events occur. - * - * @see OnFileDescriptorEventListener - * @see #removeOnFileDescriptorEventListener - */ - @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class) - public void addOnFileDescriptorEventListener(@NonNull FileDescriptor fd, - @OnFileDescriptorEventListener.Events int events, - @NonNull OnFileDescriptorEventListener listener) { - if (fd == null) { - throw new IllegalArgumentException("fd must not be null"); - } - if (listener == null) { - throw new IllegalArgumentException("listener must not be null"); - } - - synchronized (mFileDescriptorRecordsLock) { - updateOnFileDescriptorEventListenerLocked(fd, events, listener); - } - } - - /** - * Removes a file descriptor listener. - * <p> - * This method does nothing if no listener has been registered for the - * specified file descriptor. - * </p> - * - * @param fd The file descriptor whose listener will be unregistered. - * - * @see OnFileDescriptorEventListener - * @see #addOnFileDescriptorEventListener - */ - @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class) - public void removeOnFileDescriptorEventListener(@NonNull FileDescriptor fd) { - if (fd == null) { - throw new IllegalArgumentException("fd must not be null"); - } - - synchronized (mFileDescriptorRecordsLock) { - updateOnFileDescriptorEventListenerLocked(fd, 0, null); - } - } - - @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class) - private void updateOnFileDescriptorEventListenerLocked(FileDescriptor fd, int events, - OnFileDescriptorEventListener listener) { - final int fdNum = fd.getInt$(); - - int index = -1; - FileDescriptorRecord record = null; - if (mFileDescriptorRecords != null) { - index = mFileDescriptorRecords.indexOfKey(fdNum); - if (index >= 0) { - record = mFileDescriptorRecords.valueAt(index); - if (record != null && record.mEvents == events) { - return; - } - } - } - - if (events != 0) { - events |= OnFileDescriptorEventListener.EVENT_ERROR; - if (record == null) { - if (mFileDescriptorRecords == null) { - mFileDescriptorRecords = new SparseArray<FileDescriptorRecord>(); - } - record = new FileDescriptorRecord(fd, events, listener); - mFileDescriptorRecords.put(fdNum, record); - } else { - record.mListener = listener; - record.mEvents = events; - record.mSeq += 1; - } - nativeSetFileDescriptorEvents(mPtr, fdNum, events); - } else if (record != null) { - record.mEvents = 0; - mFileDescriptorRecords.removeAt(index); - nativeSetFileDescriptorEvents(mPtr, fdNum, 0); - } - } - - // Called from native code. - private int dispatchEvents(int fd, int events) { - // Get the file descriptor record and any state that might change. - final FileDescriptorRecord record; - final int oldWatchedEvents; - final OnFileDescriptorEventListener listener; - final int seq; - synchronized (mFileDescriptorRecordsLock) { - record = mFileDescriptorRecords.get(fd); - if (record == null) { - return 0; // spurious, no listener registered - } - - oldWatchedEvents = record.mEvents; - events &= oldWatchedEvents; // filter events based on current watched set - if (events == 0) { - return oldWatchedEvents; // spurious, watched events changed - } - - listener = record.mListener; - seq = record.mSeq; - } - - // Invoke the listener outside of the lock. - int newWatchedEvents = listener.onFileDescriptorEvents( - record.mDescriptor, events); - if (newWatchedEvents != 0) { - newWatchedEvents |= OnFileDescriptorEventListener.EVENT_ERROR; - } - - // Update the file descriptor record if the listener changed the set of - // events to watch and the listener itself hasn't been updated since. - if (newWatchedEvents != oldWatchedEvents) { - synchronized (mFileDescriptorRecordsLock) { - int index = mFileDescriptorRecords.indexOfKey(fd); - if (index >= 0 && mFileDescriptorRecords.valueAt(index) == record - && record.mSeq == seq) { - record.mEvents = newWatchedEvents; - if (newWatchedEvents == 0) { - mFileDescriptorRecords.removeAt(index); - } - } - } - } - - // Return the new set of events to watch for native code to take care of. - return newWatchedEvents; - } - - /** - * Callback interface for discovering when a thread is going to block - * waiting for more messages. - */ - public static interface IdleHandler { - /** - * Called when the message queue has run out of messages and will now - * wait for more. Return true to keep your idle handler active, false - * to have it removed. This may be called if there are still messages - * pending in the queue, but they are all scheduled to be dispatched - * after the current time. - */ - boolean queueIdle(); - } - - /** - * A listener which is invoked when file descriptor related events occur. - */ - public interface OnFileDescriptorEventListener { - /** - * File descriptor event: Indicates that the file descriptor is ready for input - * operations, such as reading. - * <p> - * The listener should read all available data from the file descriptor - * then return <code>true</code> to keep the listener active or <code>false</code> - * to remove the listener. - * </p><p> - * In the case of a socket, this event may be generated to indicate - * that there is at least one incoming connection that the listener - * should accept. - * </p><p> - * This event will only be generated if the {@link #EVENT_INPUT} event mask was - * specified when the listener was added. - * </p> - */ - public static final int EVENT_INPUT = 1 << 0; - - /** - * File descriptor event: Indicates that the file descriptor is ready for output - * operations, such as writing. - * <p> - * The listener should write as much data as it needs. If it could not - * write everything at once, then it should return <code>true</code> to - * keep the listener active. Otherwise, it should return <code>false</code> - * to remove the listener then re-register it later when it needs to write - * something else. - * </p><p> - * This event will only be generated if the {@link #EVENT_OUTPUT} event mask was - * specified when the listener was added. - * </p> - */ - public static final int EVENT_OUTPUT = 1 << 1; - - /** - * File descriptor event: Indicates that the file descriptor encountered a - * fatal error. - * <p> - * File descriptor errors can occur for various reasons. One common error - * is when the remote peer of a socket or pipe closes its end of the connection. - * </p><p> - * This event may be generated at any time regardless of whether the - * {@link #EVENT_ERROR} event mask was specified when the listener was added. - * </p> - */ - public static final int EVENT_ERROR = 1 << 2; - - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, prefix = { "EVENT_" }, value = { - EVENT_INPUT, - EVENT_OUTPUT, - EVENT_ERROR - }) - public @interface Events {} - - /** - * Called when a file descriptor receives events. - * - * @param fd The file descriptor. - * @param events The set of events that occurred: a combination of the - * {@link #EVENT_INPUT}, {@link #EVENT_OUTPUT}, and {@link #EVENT_ERROR} event masks. - * @return The new set of events to watch, or 0 to unregister the listener. - * - * @see #EVENT_INPUT - * @see #EVENT_OUTPUT - * @see #EVENT_ERROR - */ - @Events int onFileDescriptorEvents(@NonNull FileDescriptor fd, @Events int events); - } - - static final class FileDescriptorRecord { - public final FileDescriptor mDescriptor; - public int mEvents; - public OnFileDescriptorEventListener mListener; - public int mSeq; - - public FileDescriptorRecord(FileDescriptor descriptor, - int events, OnFileDescriptorEventListener listener) { - mDescriptor = descriptor; - mEvents = events; - mListener = listener; - } - } -} diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index 7ceb948945fd..0615578935a1 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -145,3 +145,13 @@ flag { purpose: PURPOSE_FEATURE } } + +flag { + namespace: "haptics" + name: "fix_vibration_thread_callback_handling" + description: "Fix how the VibrationThread handles late callbacks from the vibrator HAL" + bug: "395005081" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index d469a2f985fa..ca24c0c6c376 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -514,3 +514,12 @@ flag { description: "Force AttributionSource.myAttributionSource() to return a default device id" bug: "343121936" } + +flag { + name: "grant_read_blocked_numbers_to_system_ui_intelligence" + is_exported: true + is_fixed_read_only: true + namespace: "permissions" + description: "This flag is used to add role protection to READ_BLOCKED_NUMBERS for SYSTEM_UI_INTELLIGENCE" + bug: "354758615" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4ebfe53cab58..538283e10738 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -13017,18 +13017,16 @@ public final class Settings { * false/0. * @hide */ - @Readable public static final String REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI = "redact_otp_on_wifi"; /** - * Toggle for whether to immediately redact OTP notifications, or require the device to be - * locked for 10 minutes. Defaults to false/0 + * Time (in milliseconds) that the device should need to be locked, in order for an OTP + * notification to be redacted. Default is 10 minutes (600,000 ms) * @hide */ - @Readable - public static final String REDACT_OTP_NOTIFICATION_IMMEDIATELY = - "remove_otp_redaction_delay"; + public static final String OTP_NOTIFICATION_REDACTION_LOCK_TIME = + "otp_redaction_lock_time"; /** * These entries are considered common between the personal and the managed profile, diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 6f94c1b2d274..4cbd5beb3a8c 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -3131,6 +3131,7 @@ public class ZenModeConfig implements Parcelable { * @return null if DND is off or describeForeverCondition is false and * DND is on forever (until turned off) */ + // TODO: b/368247671 - Delete when inlining MODES_UI public static String getDescription(Context context, boolean zenOn, ZenModeConfig config, boolean describeForeverCondition) { if (!zenOn || config == null) { diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 08453d09a1ff..44c3f9a8244e 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -1072,13 +1072,27 @@ public abstract class Layout { var newBackground = determineContrastingBackgroundColor(index); var hasBgColorChanged = newBackground != bgPaint.getColor(); - if (lineNum != mLastLineNum || hasBgColorChanged) { - // Skip processing if the character is a space or a tap to avoid - // rendering an abrupt, empty rectangle. - if (Character.isWhitespace(mText.charAt(index))) { - return; - } + // Skip processing if the character is a space or a tap to avoid + // rendering an abrupt, empty rectangle. + if (TextLine.isLineEndSpace(mText.charAt(index))) { + return; + } + // To avoid highlighting emoji sequences, we use Extended_Pictgraphs as a + // heuristic. Highlighting is skipped based on code points, not glyph type + // (text vs. color), so emojis with default text presentation are + // intentionally not highlighted (numeric representation with emoji + // presentation are manually excluded). Although we process ZWJ and + // variation selectors within emoji sequences, they should not affect + // highlighting due to their zero-width nature. + var codePoint = Character.codePointAt(mText, index); + var isEmoji = Character.isEmojiComponent(codePoint) + || Character.isExtendedPictographic(codePoint); + if (isEmoji && !isStandardNumber(index)) { + return; + } + + if (lineNum != mLastLineNum || hasBgColorChanged) { // Draw what we have so far, then reset the rect and update its color drawRect(); mLineBackground.set(left, top, right, bottom); @@ -1097,6 +1111,16 @@ public abstract class Layout { drawRect(); } + private boolean isStandardNumber(int index) { + var codePoint = Character.codePointAt(mText, index); + var isNumberSignOrAsterisk = (codePoint >= '0' && codePoint <= '9') + || codePoint == '#' || codePoint == '*'; + var isColoredGlyph = index + 1 < mText.length() + && Character.codePointAt(mText, index + 1) == 0xFE0F; + + return isNumberSignOrAsterisk && !isColoredGlyph; + } + private void drawRect() { if (!mLineBackground.isEmpty()) { mLineBackground.inset(-padding, -padding); @@ -4627,6 +4651,16 @@ public abstract class Layout { * Callback for {@link #forEachCharacterBounds(int, int, int, int, CharacterBoundsListener)} */ private interface CharacterBoundsListener { + /** + * Called for each character with its bounds. + * + * @param index the index of the character + * @param lineNum the line number of the character + * @param left the left edge of the character + * @param top the top edge of the character + * @param right the right edge of the character + * @param bottom the bottom edge of the character + */ void onCharacterBounds(int index, int lineNum, float left, float top, float right, float bottom); diff --git a/core/java/android/util/apk/ApkSignatureVerifier.java b/core/java/android/util/apk/ApkSignatureVerifier.java index a4c3ed96f2ce..5910434dc692 100644 --- a/core/java/android/util/apk/ApkSignatureVerifier.java +++ b/core/java/android/util/apk/ApkSignatureVerifier.java @@ -260,9 +260,11 @@ public class ApkSignatureVerifier { Certificate[][] nonstreamingCerts = null; int v3BlockId = APK_SIGNATURE_SCHEME_DEFAULT; - // If V4 contains additional signing blocks then we need to always run v2/v3 verifier - // to figure out which block they use. - if (verifyFull || signingInfos.signingInfoBlocks.length > 0) { + // We need to always run v2/v3 verifier to figure out which block they use so we can + // return the past signers as well as the current one - the rotation chain is important + // for many callers who verify the signature origin as well as the apk integrity. + if (android.content.pm.Flags.alwaysLoadPastCertsV4() + || verifyFull || signingInfos.signingInfoBlocks.length > 0) { try { // v4 is an add-on and requires v2 or v3 signature to validate against its // certificate and digest diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java index f315f55c7ed4..3229fc63dad1 100644 --- a/core/java/android/view/AccessibilityInteractionController.java +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -50,6 +50,7 @@ import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.AccessibilityRequestPreparer; import android.view.accessibility.Flags; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.view.accessibility.IWindowSurfaceInfoCallback; import android.window.ScreenCapture; import com.android.internal.R; @@ -631,6 +632,25 @@ public final class AccessibilityInteractionController { } } + /** + * Provide info for taking a screenshot of this app window. + */ + public void getWindowSurfaceInfoClientThread(IWindowSurfaceInfoCallback callback) { + Message message = PooledLambda.obtainMessage( + AccessibilityInteractionController::getWindowSurfaceInfoUiThread, + this, callback); + mHandler.sendMessage(message); + } + + private void getWindowSurfaceInfoUiThread(IWindowSurfaceInfoCallback callback) { + try { + callback.provideWindowSurfaceInfo(mViewRootImpl.getWindowFlags(), Process.myUid(), + mViewRootImpl.getSurfaceControl()); + } catch (RemoteException re) { + // ignore - the other side will time out + } + } + public void findFocusClientThread(long accessibilityNodeId, int focusType, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 231aa6816908..3f45e29f2d43 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -108,6 +108,7 @@ public final class Display { private final String mOwnerPackageName; private final Resources mResources; private DisplayAdjustments mDisplayAdjustments; + private boolean mRefreshRateChangesRegistered; @UnsupportedAppUsage private DisplayInfo mDisplayInfo; // never null @@ -1217,6 +1218,10 @@ public final class Display { */ public float getRefreshRate() { synchronized (mLock) { + if (!mRefreshRateChangesRegistered) { + DisplayManagerGlobal.getInstance().registerForRefreshRateChanges(); + mRefreshRateChangesRegistered = true; + } updateDisplayInfoLocked(); return mDisplayInfo.getRefreshRate(); } @@ -1601,7 +1606,7 @@ public final class Display { .INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED | DisplayManagerGlobal .INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED, - ActivityThread.currentPackageName()); + ActivityThread.currentPackageName(), /* isEventFilterImplicit */ true); } } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index c174fbe0bbcd..3659e785f590 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -1299,7 +1299,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // Handle the pending show request for other insets types since the IME insets // has being requested hidden. handlePendingControlRequest(statsToken); - getImeSourceConsumer().removeSurface(); + if (!Flags.refactorInsetsController()) { + // the surface can't be removed until the end of the animation. This is handled by + // IMMS after the window was requested to be hidden. + getImeSourceConsumer().removeSurface(); + } } applyAnimation(typesReady, false /* show */, fromIme, false /* skipsCallbacks */, statsToken); diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index df680c054f56..fe868e1c43be 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -223,8 +223,14 @@ public class NotificationHeaderView extends RelativeLayout { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (notificationsRedesignTemplates()) { - mTopLineTranslation = measureCenterTranslation(mTopLineView); - mExpandButtonTranslation = measureCenterTranslation(mExpandButton); + // TODO: b/378660052 - These should never be null in practice, consider using + // requireViewById() in the onFinishInflate. + if (mTopLineView != null) { + mTopLineTranslation = measureCenterTranslation(mTopLineView); + } + if (mExpandButton != null) { + mExpandButtonTranslation = measureCenterTranslation(mExpandButton); + } } } diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index dd32947c69e4..b98f4db5dce6 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -16,6 +16,7 @@ package android.view; +import static android.view.flags.Flags.FLAG_DEPRECATE_SURFACE_VIEW_Z_ORDER_APIS; import static android.view.flags.Flags.FLAG_SURFACE_VIEW_GET_SURFACE_PACKAGE; import static android.view.flags.Flags.FLAG_SURFACE_VIEW_SET_COMPOSITION_ORDER; import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; @@ -812,7 +813,12 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall * window is attached to the window manager. * * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}. + * + * @deprecated Use {@link #setCompositionOrder(int)} instead. It provides more + * control over the Z ordering behavior. */ + @Deprecated + @FlaggedApi(FLAG_DEPRECATE_SURFACE_VIEW_Z_ORDER_APIS) public void setZOrderMediaOverlay(boolean isMediaOverlay) { mRequestedSubLayer = isMediaOverlay ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER; @@ -834,7 +840,12 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. * * @param onTop Whether to show the surface on top of this view's window. + * + * @deprecated Use {@link #setCompositionOrder(int)} instead. It provides more + * control over the Z ordering behavior. */ + @Deprecated + @FlaggedApi(FLAG_DEPRECATE_SURFACE_VIEW_Z_ORDER_APIS) public void setZOrderOnTop(boolean onTop) { // In R and above we allow dynamic layer changes. final boolean allowDynamicChange = getContext().getApplicationInfo().targetSdkVersion @@ -866,7 +877,11 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall * @return Whether the Z ordering changed. * * @hide + * + * @deprecated Use {@link #setCompositionOrder(int)} instead. It provides more control + * over the Z ordering behavior. */ + @Deprecated public boolean setZOrderedOnTop(boolean onTop, boolean allowDynamicChange) { final int subLayer; if (onTop) { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 0866e0d832b1..c048d7987515 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -66,6 +66,7 @@ import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFI import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION; import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS; import static com.android.window.flags.Flags.FLAG_SUPPORTS_DRAG_ASSISTANT_TO_MULTIWINDOW; +import static com.android.window.flags.Flags.reduceChangedExclusionRectsMsgs; import static java.lang.Math.max; @@ -247,6 +248,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; @@ -12888,15 +12890,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (rects.isEmpty() && mListenerInfo == null) return; final ListenerInfo info = getListenerInfo(); + final boolean rectsChanged = !reduceChangedExclusionRectsMsgs() + || !Objects.equals(info.mSystemGestureExclusionRects, rects); if (info.mSystemGestureExclusionRects != null) { - info.mSystemGestureExclusionRects.clear(); - info.mSystemGestureExclusionRects.addAll(rects); + if (rectsChanged) { + info.mSystemGestureExclusionRects.clear(); + info.mSystemGestureExclusionRects.addAll(rects); + } } else { info.mSystemGestureExclusionRects = new ArrayList<>(rects); } - - updatePositionUpdateListener(); - postUpdate(this::updateSystemGestureExclusionRects); + if (rectsChanged) { + updatePositionUpdateListener(); + postUpdate(this::updateSystemGestureExclusionRects); + } } private void updatePositionUpdateListener() { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 6b6147a3749d..9498407fb33b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -136,6 +136,7 @@ import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme; import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay; import static com.android.window.flags.Flags.enableWindowContextResourcesUpdateOnConfigChange; import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi; +import static com.android.window.flags.Flags.reduceChangedExclusionRectsMsgs; import static com.android.window.flags.Flags.setScPropertiesInClient; import android.Manifest; @@ -152,8 +153,6 @@ import android.annotation.UiContext; import android.app.ActivityManager; import android.app.ActivityThread; import android.app.ResourcesManager; -import android.app.UiModeManager; -import android.app.UiModeManager.ForceInvertStateChangeListener; import android.app.WindowConfiguration; import android.app.compat.CompatChanges; import android.app.servertransaction.WindowStateTransactionItem; @@ -169,6 +168,7 @@ import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; +import android.database.ContentObserver; import android.graphics.BLASTBufferQueue; import android.graphics.Canvas; import android.graphics.Color; @@ -214,6 +214,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.os.Vibrator; +import android.provider.Settings; import android.sysprop.DisplayProperties; import android.sysprop.ViewProperties; import android.text.TextUtils; @@ -254,6 +255,7 @@ import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityEmbeddedConnection; import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.view.accessibility.IWindowSurfaceInfoCallback; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.view.autofill.AutofillManager; @@ -468,8 +470,10 @@ public final class ViewRootImpl implements ViewParent, private CompatOnBackInvokedCallback mCompatOnBackInvokedCallback; @Nullable - private ForceInvertStateChangeListener mForceInvertStateChangeListener; + private ContentObserver mForceInvertObserver; + private static final int INVALID_VALUE = Integer.MIN_VALUE; + private int mForceInvertEnabled = INVALID_VALUE; /** * Callback for notifying about global configuration changes. */ @@ -551,8 +555,6 @@ public final class ViewRootImpl implements ViewParent, @UiContext public final Context mContext; - private UiModeManager mUiModeManager; - @UnsupportedAppUsage final IWindowSession mWindowSession; @NonNull Display mDisplay; @@ -1254,7 +1256,6 @@ public final class ViewRootImpl implements ViewParent, public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session, WindowLayout windowLayout) { mContext = context; - mUiModeManager = context.getSystemService(UiModeManager.class); mWindowSession = session; mWindowLayout = windowLayout; mDisplay = display; @@ -1803,6 +1804,23 @@ public final class ViewRootImpl implements ViewParent, } } + private boolean isForceInvertEnabled() { + if (mForceInvertEnabled == INVALID_VALUE) { + reloadForceInvertEnabled(); + } + return mForceInvertEnabled == 1; + } + + private void reloadForceInvertEnabled() { + if (forceInvertColor()) { + mForceInvertEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, + /* def= */ 0, + UserHandle.myUserId()); + } + } + /** * Register any kind of listeners if setView was success. */ @@ -1834,11 +1852,21 @@ public final class ViewRootImpl implements ViewParent, mBasePackageName); if (forceInvertColor()) { - if (mForceInvertStateChangeListener == null) { - mForceInvertStateChangeListener = - forceInvertState -> updateForceDarkMode(); - mUiModeManager.addForceInvertStateChangeListener(mExecutor, - mForceInvertStateChangeListener); + if (mForceInvertObserver == null) { + mForceInvertObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + reloadForceInvertEnabled(); + updateForceDarkMode(); + } + }; + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED + ), + false, + mForceInvertObserver, + UserHandle.myUserId()); } } } @@ -1856,10 +1884,9 @@ public final class ViewRootImpl implements ViewParent, .unregisterDisplayListener(mDisplayListener); if (forceInvertColor()) { - if (mForceInvertStateChangeListener != null) { - mUiModeManager.removeForceInvertStateChangeListener( - mForceInvertStateChangeListener); - mForceInvertStateChangeListener = null; + if (mForceInvertObserver != null) { + mContext.getContentResolver().unregisterContentObserver(mForceInvertObserver); + mForceInvertObserver = null; } } @@ -2046,25 +2073,21 @@ public final class ViewRootImpl implements ViewParent, return getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; } - /** - * Determines the type of force dark to apply, considering force inversion, system night mode, - * and app-specific settings (including developer opt-outs). - * - * @return A {@link ForceDarkType.ForceDarkTypeDef} constant indicating the force dark type. - */ + /** Returns true if force dark should be enabled according to various settings */ @VisibleForTesting public @ForceDarkType.ForceDarkTypeDef int determineForceDarkType() { if (forceInvertColor()) { // Force invert ignores all developer opt-outs. // We also ignore dark theme, since the app developer can override the user's preference - // for dark mode in configuration.uiMode. Instead, we assume that both force invert and - // the system's dark theme are enabled. - if (mUiModeManager.getForceInvertState() == UiModeManager.FORCE_INVERT_TYPE_DARK) { + // for dark mode in configuration.uiMode. Instead, we assume that the force invert + // setting will be enabled at the same time dark theme is in the Settings app. + if (isForceInvertEnabled()) { return ForceDarkType.FORCE_INVERT_COLOR_DARK; } } boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES; + if (useAutoDark) { boolean forceDarkAllowedDefault = SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false); @@ -6052,8 +6075,12 @@ public final class ViewRootImpl implements ViewParent, } void updateSystemGestureExclusionRectsForView(View view) { + boolean msgInQueue = reduceChangedExclusionRectsMsgs() + && mGestureExclusionTracker.isWaitingForComputeChanges(); mGestureExclusionTracker.updateRectsForView(view); - mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED); + if (!msgInQueue) { + mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED); + } } void systemGestureExclusionChanged() { @@ -6097,8 +6124,12 @@ public final class ViewRootImpl implements ViewParent, * the root's view hierarchy. */ public void setRootSystemGestureExclusionRects(@NonNull List<Rect> rects) { + boolean msgInQueue = reduceChangedExclusionRectsMsgs() + && mGestureExclusionTracker.isWaitingForComputeChanges(); mGestureExclusionTracker.setRootRects(rects); - mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED); + if (!msgInQueue) { + mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED); + } } /** @@ -12285,6 +12316,15 @@ public final class ViewRootImpl implements ViewParent, } } + @Override + public void getWindowSurfaceInfo(IWindowSurfaceInfoCallback callback) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { + viewRootImpl.getAccessibilityInteractionController() + .getWindowSurfaceInfoClientThread(callback); + } + } + public void attachAccessibilityOverlayToWindow( SurfaceControl sc, int interactionId, diff --git a/core/java/android/view/ViewRootRectTracker.java b/core/java/android/view/ViewRootRectTracker.java index 152729b8d1d8..0bef3255f1dc 100644 --- a/core/java/android/view/ViewRootRectTracker.java +++ b/core/java/android/view/ViewRootRectTracker.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import java.lang.ref.WeakReference; @@ -32,8 +33,10 @@ import java.util.function.Function; /** * Abstract class to track a collection of rects reported by the views under the same * {@link ViewRootImpl}. + * @hide */ -class ViewRootRectTracker { +@VisibleForTesting +public class ViewRootRectTracker { private final Function<View, List<Rect>> mRectCollector; private boolean mViewsChanged = false; private boolean mRootRectsChanged = false; @@ -41,11 +44,18 @@ class ViewRootRectTracker { private List<ViewInfo> mViewInfos = new ArrayList<>(); private List<Rect> mRects = Collections.emptyList(); + // Keeps track of whether updateRectsForView has been called but there was no subsequent call + // on computeChanges yet. Since updateRectsForView is called when sending a message and + // computeChanges when it is received, this tracks whether such message is in the queue already + private boolean mWaitingForComputeChanges = false; + /** * @param rectCollector given a view returns a list of the rects of interest for this * ViewRootRectTracker + * @hide */ - ViewRootRectTracker(Function<View, List<Rect>> rectCollector) { + @VisibleForTesting + public ViewRootRectTracker(Function<View, List<Rect>> rectCollector) { mRectCollector = rectCollector; } @@ -70,6 +80,7 @@ class ViewRootRectTracker { mViewInfos.add(new ViewInfo(view)); mViewsChanged = true; } + mWaitingForComputeChanges = true; } /** @@ -92,6 +103,7 @@ class ViewRootRectTracker { * @return {@code true} if there were changes, {@code false} otherwise. */ public boolean computeChanges() { + mWaitingForComputeChanges = false; boolean changed = mRootRectsChanged; final Iterator<ViewInfo> i = mViewInfos.iterator(); final List<Rect> rects = new ArrayList<>(mRootRects); @@ -121,6 +133,10 @@ class ViewRootRectTracker { return false; } + public boolean isWaitingForComputeChanges() { + return mWaitingForComputeChanges; + } + /** * Returns a List of all Rects from all visible Views in the global (root) coordinate system. * This list is only updated when calling {@link #computeChanges()} or @@ -140,6 +156,7 @@ class ViewRootRectTracker { Preconditions.checkNotNull(rects, "rects must not be null"); mRootRects = rects; mRootRectsChanged = true; + mWaitingForComputeChanges = true; } @NonNull diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java index e3ea6b229d64..a952967f4daf 100644 --- a/core/java/android/view/WindowInsets.java +++ b/core/java/android/view/WindowInsets.java @@ -1290,7 +1290,8 @@ public final class WindowInsets { int newTop = Math.max(0, insets.top - top); int newRight = Math.max(0, insets.right - right); int newBottom = Math.max(0, insets.bottom - bottom); - if (newLeft == left && newTop == top && newRight == right && newBottom == bottom) { + if (newLeft == insets.left && newTop == insets.top + && newRight == insets.right && newBottom == insets.bottom) { return insets; } return Insets.of(newLeft, newTop, newRight, newBottom); diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl index a43acf9147f3..ca024fd03778 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -23,6 +23,7 @@ import android.view.MagnificationSpec; import android.view.SurfaceControl; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.view.accessibility.IWindowSurfaceInfoCallback; import android.window.ScreenCapture; /** @@ -67,5 +68,7 @@ oneway interface IAccessibilityInteractionConnection { in ScreenCapture.ScreenCaptureListener listener, IAccessibilityInteractionConnectionCallback callback); + void getWindowSurfaceInfo(IWindowSurfaceInfoCallback callback); + void attachAccessibilityOverlayToWindow(in SurfaceControl sc, int interactionId, in IAccessibilityInteractionConnectionCallback callback); } diff --git a/core/java/android/view/accessibility/IWindowSurfaceInfoCallback.aidl b/core/java/android/view/accessibility/IWindowSurfaceInfoCallback.aidl new file mode 100644 index 000000000000..370f185b5596 --- /dev/null +++ b/core/java/android/view/accessibility/IWindowSurfaceInfoCallback.aidl @@ -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 android.view.accessibility; + +import android.view.SurfaceControl; + +/** + * Callback for an app to provide AbstractAccessibilityServiceConnection with window and surface + * information necessary for system_server to take a screenshot of the app window. + * @hide + */ +oneway interface IWindowSurfaceInfoCallback { + + /** + * Provide info from ViewRootImpl for taking a screenshot of this app window. + * + * @param windowFlags the window flags of ViewRootImpl + * @param processUid the process (kernel) uid, NOT the user ID, required for + SurfaceFlinger screenshots + * @param surfaceControl the surface of ViewRootImpl + */ + @RequiresNoPermission + void provideWindowSurfaceInfo(int windowFlags, int processUid, + in SurfaceControl surfaceControl); +} diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig index f6fdec94c332..d06f885638b6 100644 --- a/core/java/android/view/flags/view_flags.aconfig +++ b/core/java/android/view/flags/view_flags.aconfig @@ -129,6 +129,14 @@ flag { } flag { + name: "deprecate_surface_view_z_order_apis" + namespace: "window_surfaces" + description: "Deprecate SurfaceView z order control APIs." + bug: "341021569" + is_fixed_read_only: true +} + +flag { name: "use_refactored_round_scrollbar" namespace: "wear_frameworks" description: "Use refactored round scrollbar." diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index b44620f12bbb..50b7bb8ce9da 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -102,9 +102,12 @@ public enum DesktopModeFlags { ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true), ENABLE_MINIMIZE_BUTTON(Flags::enableMinimizeButton, true), ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS(Flags::enableModalsFullscreenWithPermission, false), + ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS( + Flags::enableOpaqueBackgroundForTransparentWindows, false), ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true), ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE( Flags::enableRestoreToPreviousSizeFromDesktopImmersive, true), + ENABLE_TASKBAR_OVERFLOW(Flags::enableTaskbarOverflow, false), ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS(Flags::enableTaskResizingKeyboardShortcuts, true), ENABLE_TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true), ENABLE_THEMED_APP_HEADERS(Flags::enableThemedAppHeaders, true), diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 891c5e382a58..f6ed50e866fd 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -196,6 +196,16 @@ flag { } flag { + name: "enable_camera_compat_for_desktop_windowing_opt_out" + namespace: "lse_desktop_experience" + description: "Whether to allow developers to opt-out of Camera Compat treatment to fixed-orientation apps in desktop windowing mode" + bug: "328616176" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_task_stack_observer_in_shell" namespace: "lse_desktop_experience" description: "Introduces a new observer in shell to track the task stack." @@ -671,6 +681,16 @@ flag { } flag { + name: "enable_opaque_background_for_transparent_windows" + namespace: "lse_desktop_experience" + description: "Whether an opaque background should be forcefully set for windows with only transparent background." + bug: "397219542" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_desktop_mode_through_dev_option" namespace: "lse_desktop_experience" description: "Enables support for desktop mode through developer options for devices eligible for desktop mode." @@ -695,6 +715,13 @@ flag { } flag { + name: "enable_activity_embedding_support_for_connected_displays" + namespace: "lse_desktop_experience" + description: "Enables activity embedding support for connected displays, including enabling AE optimization for Settings." + bug: "369438353" +} + +flag { name: "enable_full_screen_window_on_removing_split_screen_stage_bugfix" namespace: "lse_desktop_experience" description: "Enables clearing the windowing mode of a freeform window when removing the task from the split screen stage." @@ -754,3 +781,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_taskbar_overflow" + namespace: "lse_desktop_experience" + description: "Show recent apps in the taskbar overflow." + bug: "368119679" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 60f2c811dd1f..a4d128fa3caf 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -428,6 +428,17 @@ flag { } flag { + name: "reduce_changed_exclusion_rects_msgs" + namespace: "windowing_frontend" + description: "Don't send MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED when there is no change" + bug: "388231176" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "keep_app_window_hide_while_locked" namespace: "windowing_frontend" description: "Do not let app window visible while device is locked" diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 3bdf3d65e99c..c009fc3b7e63 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -1875,9 +1875,7 @@ public class ChooserActivity extends ResolverActivity implements Bundle b = new Bundle(); // Add userHandle based badge to the stackedAppDialogBox. b.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY, - getResolveInfoUserHandle( - targetInfo.getResolveInfo(), - mChooserMultiProfilePagerAdapter.getCurrentUserHandle())); + targetInfo.getResolveInfo().userHandle); b.putObject(ChooserStackedAppDialogFragment.MULTI_DRI_KEY, mti); b.putInt(ChooserStackedAppDialogFragment.WHICH_KEY, which); @@ -2457,10 +2455,7 @@ public class ChooserActivity extends ResolverActivity implements // compares using resolveInfo.userHandle mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator) .thenComparingInt(displayResolveInfo -> - getResolveInfoUserHandle( - displayResolveInfo.getResolveInfo(), - // TODO: User resolveInfo.userHandle, once its available. - UserHandle.SYSTEM).getIdentifier()); + displayResolveInfo.getResolveInfo().userHandle.getIdentifier()); } @Override diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java index b3e828d15737..d38689c7505b 100644 --- a/core/java/com/android/internal/app/ChooserListAdapter.java +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -362,8 +362,7 @@ public class ChooserListAdapter extends ResolverListAdapter { } String resolvedTarget = info.getResolvedComponentName().getPackageName() + '#' + info.getDisplayLabel() - + '#' + ResolverActivity.getResolveInfoUserHandle( - info.getResolveInfo(), getUserHandle()).getIdentifier(); + + '#' + info.getResolveInfo().userHandle.getIdentifier(); DisplayResolveInfo multiDri = consolidated.get(resolvedTarget); if (multiDri == null) { consolidated.put(resolvedTarget, info); diff --git a/core/java/com/android/internal/app/MediaRouteControllerContentManager.java b/core/java/com/android/internal/app/MediaRouteControllerContentManager.java index 83ae7edd796b..3a8b94f222ba 100644 --- a/core/java/com/android/internal/app/MediaRouteControllerContentManager.java +++ b/core/java/com/android/internal/app/MediaRouteControllerContentManager.java @@ -17,7 +17,12 @@ package com.android.internal.app; import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; import android.media.MediaRouter; +import android.util.TypedValue; import android.view.View; import android.widget.LinearLayout; import android.widget.SeekBar; @@ -28,19 +33,56 @@ import com.android.internal.R; * This class manages the content display within the media route controller UI. */ public class MediaRouteControllerContentManager { + /** + * A delegate interface that a MediaRouteController UI should implement. It allows the content + * manager to inform the UI of any UI changes that need to be made in response to content + * updates. + */ + public interface Delegate { + /** + * Updates the title of the media route device + */ + void setMediaRouteDeviceTitle(CharSequence title); + + /** + * Updates the icon of the media route device + */ + void setMediaRouteDeviceIcon(Drawable icon); + + /** + * Dismiss the UI to transition to a different workflow. + */ + void dismissView(); + } + + private final Context mContext; + private final Delegate mDelegate; + // Time to wait before updating the volume when the user lets go of the seek bar // to allow the route provider time to propagate the change and publish a new // route descriptor. private static final int VOLUME_UPDATE_DELAY_MILLIS = 250; + private final MediaRouter mRouter; + private final MediaRouteControllerContentManager.MediaRouterCallback mCallback; private final MediaRouter.RouteInfo mRoute; + private Drawable mMediaRouteButtonDrawable; + private final int[] mMediaRouteConnectingState = { R.attr.state_checked, R.attr.state_enabled }; + private final int[] mMediaRouteOnState = { R.attr.state_activated, R.attr.state_enabled }; + private Drawable mCurrentIconDrawable; + + private boolean mAttachedToWindow; + private LinearLayout mVolumeLayout; private SeekBar mVolumeSlider; private boolean mVolumeSliderTouched; - public MediaRouteControllerContentManager(Context context) { - MediaRouter mRouter = context.getSystemService(MediaRouter.class); + public MediaRouteControllerContentManager(Context context, Delegate delegate) { + mContext = context; + mDelegate = delegate; + mRouter = context.getSystemService(MediaRouter.class); + mCallback = new MediaRouteControllerContentManager.MediaRouterCallback(); mRoute = mRouter.getSelectedRoute(); } @@ -49,6 +91,7 @@ public class MediaRouteControllerContentManager { * given container view. */ public void bindViews(View containerView) { + mDelegate.setMediaRouteDeviceTitle(mRoute.getName()); mVolumeLayout = containerView.findViewById(R.id.media_route_volume_layout); mVolumeSlider = containerView.findViewById(R.id.media_route_volume_slider); mVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @@ -86,12 +129,58 @@ public class MediaRouteControllerContentManager { } } }); + + mMediaRouteButtonDrawable = obtainMediaRouteButtonDrawable(); + } + + /** + * Called when this UI is attached to a window.. + */ + public void onAttachedToWindow() { + mAttachedToWindow = true; + mRouter.addCallback(0, mCallback, MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS); + update(); } /** - * Updates the volume layout and slider. + * Called when this UI is detached from a window.. */ - public void updateVolume() { + public void onDetachedFromWindow() { + mRouter.removeCallback(mCallback); + mAttachedToWindow = false; + } + + /** + * Updates all the views to reflect new states. + */ + public void update() { + if (!mRoute.isSelected() || mRoute.isDefault()) { + mDelegate.dismissView(); + } + + mDelegate.setMediaRouteDeviceTitle(mRoute.getName()); + updateVolume(); + + Drawable icon = getIconDrawable(); + if (icon != mCurrentIconDrawable) { + mCurrentIconDrawable = icon; + if (icon instanceof AnimationDrawable animDrawable) { + if (!mAttachedToWindow && !mRoute.isConnecting()) { + // When the route is already connected before the view is attached, show the + // last frame of the connected animation immediately. + if (animDrawable.isRunning()) { + animDrawable.stop(); + } + icon = animDrawable.getFrame(animDrawable.getNumberOfFrames() - 1); + } else if (!animDrawable.isRunning()) { + animDrawable.start(); + } + } + mDelegate.setMediaRouteDeviceIcon(icon); + } + } + + private void updateVolume() { if (!mVolumeSliderTouched) { if (isVolumeControlAvailable()) { mVolumeLayout.setVisibility(View.VISIBLE); @@ -103,7 +192,78 @@ public class MediaRouteControllerContentManager { } } + /** + * Callback function to triggered after the disconnect button is clicked. + */ + public void onDisconnectButtonClick() { + if (mRoute.isSelected()) { + if (mRoute.isBluetooth()) { + mRouter.getDefaultRoute().select(); + } else { + mRouter.getFallbackRoute().select(); + } + } + mDelegate.dismissView(); + } + private boolean isVolumeControlAvailable() { return mRoute.getVolumeHandling() == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE; } + + private Drawable obtainMediaRouteButtonDrawable() { + TypedValue value = new TypedValue(); + if (!mContext.getTheme().resolveAttribute(R.attr.mediaRouteButtonStyle, value, true)) { + return null; + } + int[] drawableAttrs = new int[] { R.attr.externalRouteEnabledDrawable }; + TypedArray a = mContext.obtainStyledAttributes(value.data, drawableAttrs); + Drawable drawable = a.getDrawable(0); + a.recycle(); + return drawable; + } + + private Drawable getIconDrawable() { + if (!(mMediaRouteButtonDrawable instanceof StateListDrawable)) { + return mMediaRouteButtonDrawable; + } else if (mRoute.isConnecting()) { + StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable; + stateListDrawable.setState(mMediaRouteConnectingState); + return stateListDrawable.getCurrent(); + } else { + StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable; + stateListDrawable.setState(mMediaRouteOnState); + return stateListDrawable.getCurrent(); + } + } + + private final class MediaRouterCallback extends MediaRouter.SimpleCallback { + @Override + public void onRouteUnselected(MediaRouter router, int type, MediaRouter.RouteInfo info) { + update(); + } + + @Override + public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { + update(); + } + + @Override + public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) { + if (route == mRoute) { + updateVolume(); + } + } + + @Override + public void onRouteGrouped(MediaRouter router, MediaRouter.RouteInfo info, + MediaRouter.RouteGroup group, int index) { + update(); + } + + @Override + public void onRouteUngrouped(MediaRouter router, MediaRouter.RouteInfo info, + MediaRouter.RouteGroup group) { + update(); + } + } } diff --git a/core/java/com/android/internal/app/MediaRouteControllerDialog.java b/core/java/com/android/internal/app/MediaRouteControllerDialog.java index c79f3c7bf76f..5899963f4550 100644 --- a/core/java/com/android/internal/app/MediaRouteControllerDialog.java +++ b/core/java/com/android/internal/app/MediaRouteControllerDialog.java @@ -21,15 +21,9 @@ import android.app.MediaRouteActionProvider; import android.app.MediaRouteButton; import android.content.Context; import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; import android.media.MediaRouter; -import android.media.MediaRouter.RouteGroup; -import android.media.MediaRouter.RouteInfo; import android.os.Bundle; -import android.util.TypedValue; import android.view.KeyEvent; import android.view.View; @@ -46,73 +40,50 @@ import com.android.internal.R; * * TODO: Move this back into the API, as in the support library media router. */ -public class MediaRouteControllerDialog extends AlertDialog { +public class MediaRouteControllerDialog extends AlertDialog implements + MediaRouteControllerContentManager.Delegate { + // TODO(b/360050020): Eventually these 2 variables should be in the content manager instead of + // here. So these should be removed when the migration is completed. private final MediaRouter mRouter; - private final MediaRouterCallback mCallback; private final MediaRouter.RouteInfo mRoute; - private Drawable mMediaRouteButtonDrawable; - private int[] mMediaRouteConnectingState = { R.attr.state_checked, R.attr.state_enabled }; - private int[] mMediaRouteOnState = { R.attr.state_activated, R.attr.state_enabled }; - private Drawable mCurrentIconDrawable; - - private boolean mAttachedToWindow; - private final MediaRouteControllerContentManager mContentManager; public MediaRouteControllerDialog(Context context, int theme) { super(context, theme); - mContentManager = new MediaRouteControllerContentManager(context); + mContentManager = new MediaRouteControllerContentManager(context, this); mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); - mCallback = new MediaRouterCallback(); mRoute = mRouter.getSelectedRoute(); } @Override protected void onCreate(Bundle savedInstanceState) { - setTitle(mRoute.getName()); Resources res = getContext().getResources(); setButton(BUTTON_NEGATIVE, res.getString(R.string.media_route_controller_disconnect), - (dialogInterface, id) -> { - if (mRoute.isSelected()) { - if (mRoute.isBluetooth()) { - mRouter.getDefaultRoute().select(); - } else { - mRouter.getFallbackRoute().select(); - } - } - dismiss(); - }); + (dialogInterface, id) -> mContentManager.onDisconnectButtonClick()); View customView = getLayoutInflater().inflate(R.layout.media_route_controller_dialog, null); setView(customView, 0, 0, 0, 0); - super.onCreate(savedInstanceState); - mContentManager.bindViews(customView); + super.onCreate(savedInstanceState); View customPanelView = getWindow().findViewById(R.id.customPanel); if (customPanelView != null) { customPanelView.setMinimumHeight(0); } - mMediaRouteButtonDrawable = obtainMediaRouteButtonDrawable(); - update(); + mContentManager.update(); } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); - mAttachedToWindow = true; - - mRouter.addCallback(0, mCallback, MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS); - update(); + mContentManager.onAttachedToWindow(); } @Override public void onDetachedFromWindow() { - mRouter.removeCallback(mCallback); - mAttachedToWindow = false; - + mContentManager.onDetachedFromWindow(); super.onDetachedFromWindow(); } @@ -135,87 +106,18 @@ public class MediaRouteControllerDialog extends AlertDialog { return super.onKeyUp(keyCode, event); } - private void update() { - if (!mRoute.isSelected() || mRoute.isDefault()) { - dismiss(); - } - - setTitle(mRoute.getName()); - mContentManager.updateVolume(); - - Drawable icon = getIconDrawable(); - if (icon != mCurrentIconDrawable) { - mCurrentIconDrawable = icon; - if (icon instanceof AnimationDrawable animDrawable) { - if (!mAttachedToWindow && !mRoute.isConnecting()) { - // When the route is already connected before the view is attached, show the - // last frame of the connected animation immediately. - if (animDrawable.isRunning()) { - animDrawable.stop(); - } - icon = animDrawable.getFrame(animDrawable.getNumberOfFrames() - 1); - } else if (!animDrawable.isRunning()) { - animDrawable.start(); - } - } - setIcon(icon); - } - } - - private Drawable obtainMediaRouteButtonDrawable() { - Context context = getContext(); - TypedValue value = new TypedValue(); - if (!context.getTheme().resolveAttribute(R.attr.mediaRouteButtonStyle, value, true)) { - return null; - } - int[] drawableAttrs = new int[] { R.attr.externalRouteEnabledDrawable }; - TypedArray a = context.obtainStyledAttributes(value.data, drawableAttrs); - Drawable drawable = a.getDrawable(0); - a.recycle(); - return drawable; + @Override + public void setMediaRouteDeviceTitle(CharSequence title) { + setTitle(title); } - private Drawable getIconDrawable() { - if (!(mMediaRouteButtonDrawable instanceof StateListDrawable)) { - return mMediaRouteButtonDrawable; - } else if (mRoute.isConnecting()) { - StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable; - stateListDrawable.setState(mMediaRouteConnectingState); - return stateListDrawable.getCurrent(); - } else { - StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable; - stateListDrawable.setState(mMediaRouteOnState); - return stateListDrawable.getCurrent(); - } + @Override + public void setMediaRouteDeviceIcon(Drawable icon) { + setIcon(icon); } - private final class MediaRouterCallback extends MediaRouter.SimpleCallback { - @Override - public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { - update(); - } - - @Override - public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { - update(); - } - - @Override - public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) { - if (route == mRoute) { - mContentManager.updateVolume(); - } - } - - @Override - public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, - int index) { - update(); - } - - @Override - public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { - update(); - } + @Override + public void dismissView() { + dismiss(); } } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index db65d31f59da..eaf1573e929f 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -1605,8 +1605,7 @@ public class ResolverActivity extends Activity implements // In case cloned apps are present, we would want to start those apps in cloned user // space, which will not be same as adaptor's userHandle. resolveInfo.userHandle // identifies the correct user space in such cases. - UserHandle activityUserHandle = getResolveInfoUserHandle( - cti.getResolveInfo(), mMultiProfilePagerAdapter.getCurrentUserHandle()); + UserHandle activityUserHandle = cti.getResolveInfo().userHandle; safelyStartActivityAsUser(cti, activityUserHandle, null); } @@ -2399,11 +2398,7 @@ public class ResolverActivity extends Activity implements && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName) // Comparing against resolveInfo.userHandle in case cloned apps are present, // as they will have the same activityInfo. - && Objects.equals( - getResolveInfoUserHandle(lhs, - mMultiProfilePagerAdapter.getActiveListAdapter().getUserHandle()), - getResolveInfoUserHandle(rhs, - mMultiProfilePagerAdapter.getActiveListAdapter().getUserHandle())); + && Objects.equals(lhs.userHandle, rhs.userHandle); } protected String getMetricsCategory() { @@ -2686,18 +2681,6 @@ public class ResolverActivity extends Activity implements return userList; } - /** - * This function is temporary in nature, and its usages will be replaced with just - * resolveInfo.userHandle, once it is available, once sharesheet is stable. - */ - public static UserHandle getResolveInfoUserHandle(ResolveInfo resolveInfo, - UserHandle predictedHandle) { - if (resolveInfo.userHandle == null) { - Log.e(TAG, "ResolveInfo with null UserHandle found: " + resolveInfo); - } - return resolveInfo.userHandle; - } - private boolean privateSpaceEnabled() { return mIsIntentPicker && android.os.Flags.allowPrivateProfile() && android.multiuser.Flags.allowResolverSheetForPrivateSpace() diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index de7ad346a7cd..54c0e61fd5cd 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -747,8 +747,7 @@ public class ResolverListAdapter extends BaseAdapter { Drawable loadIconForResolveInfo(ResolveInfo ri) { // Load icons based on userHandle from ResolveInfo. If in work profile/clone profile, icons // should be badged. - return makePresentationGetter(ri) - .getIcon(ResolverActivity.getResolveInfoUserHandle(ri, getUserHandle())); + return makePresentationGetter(ri).getIcon(ri.userHandle); } void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) { diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java index 454323b60333..a0f45b082a36 100644 --- a/core/java/com/android/internal/jank/FrameTracker.java +++ b/core/java/com/android/internal/jank/FrameTracker.java @@ -257,15 +257,11 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai @Override public void surfaceDestroyed() { - - // Wait a while to give the system a chance for the remaining - // frames to arrive, then force finish the session. - mHandler.postDelayed(() -> { + mHandler.post(() -> { if (!mMetricsFinalized) { end(REASON_END_SURFACE_DESTROYED); - finish(); } - }, 50); + }); } }; // This callback has a reference to FrameTracker, @@ -367,9 +363,9 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai // Send a flush jank data transaction. if (mSurfaceControl != null && mSurfaceControl.isValid()) { SurfaceControl.Transaction.sendSurfaceFlushJankData(mSurfaceControl); - if (mJankDataListenerRegistration != null) { - mJankDataListenerRegistration.flush(); - } + } + if (mJankDataListenerRegistration != null) { + mJankDataListenerRegistration.flush(); } long delay; @@ -650,6 +646,8 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai Log.w(TAG, "Missing SF jank callback for vsyncId: " + info.frameVsyncId + ", CUJ=" + name); } + } else if (Flags.useSfFrameDuration() && info.surfaceControlCallbackFired) { + maxFrameTimeNanos = Math.max(info.totalDurationNanos, maxFrameTimeNanos); } } maxSuccessiveMissedFramesCount = Math.max( diff --git a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java index 2931bd2c83dd..5e9c87a5154a 100644 --- a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java +++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java @@ -16,6 +16,8 @@ package com.android.internal.os; +import android.annotation.CheckResult; +import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; import android.ravenwood.annotation.RavenwoodKeepWholeClass; @@ -155,8 +157,9 @@ public final class LongArrayMultiStateCounter implements Parcelable { /** * Adds the supplied values to the current accumulated values in the counter. + * Null `values` parameter is interpreted as an array of zeros. */ - public void incrementValues(long[] values, long timestampMs) { + public void incrementValues(@Nullable long[] values, long timestampMs) { native_incrementValues(mNativeObject, values, timestampMs); } @@ -194,7 +197,8 @@ public final class LongArrayMultiStateCounter implements Parcelable { /** * Populates the array with the accumulated counts for the specified state. */ - public void getCounts(long[] counts, int state) { + @CheckResult + public boolean getCounts(long[] counts, int state) { if (state < 0 || state >= mStateCount) { throw new IllegalArgumentException( "State: " + state + ", outside the range: [0-" + mStateCount + "]"); @@ -203,7 +207,7 @@ public final class LongArrayMultiStateCounter implements Parcelable { throw new IllegalArgumentException( "Invalid array length: " + counts.length + ", expected: " + mLength); } - native_getCounts(mNativeObject, counts, state); + return native_getCounts(mNativeObject, counts, state); } @Override @@ -280,7 +284,8 @@ public final class LongArrayMultiStateCounter implements Parcelable { @FastNative @RavenwoodRedirect - private static native void native_getCounts(long nativeObject, long[] counts, int state); + @CheckResult + private static native boolean native_getCounts(long nativeObject, long[] counts, int state); @FastNative @RavenwoodRedirect diff --git a/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java index 7030d8e84b70..403e8c1be8fa 100644 --- a/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java +++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java @@ -16,6 +16,7 @@ package com.android.internal.os; +import android.annotation.Nullable; import android.os.BadParcelableException; import android.os.Parcel; import android.ravenwood.annotation.RavenwoodKeepWholeClass; @@ -147,10 +148,12 @@ class LongArrayMultiStateCounter_ravenwood { mLastUpdateTimestampMs = timestampMs; } - public void incrementValues(long[] delta, long timestampMs) { + public void incrementValues(@Nullable long[] delta, long timestampMs) { long[] values = Arrays.copyOf(mValues, mValues.length); - for (int i = 0; i < mArrayLength; i++) { - values[i] += delta[i]; + if (delta != null) { + for (int i = 0; i < mArrayLength; i++) { + values[i] += delta[i]; + } } updateValue(values, timestampMs); } @@ -165,8 +168,20 @@ class LongArrayMultiStateCounter_ravenwood { } } - public void getValues(long[] values, int state) { - System.arraycopy(mStates[state].mCounter, 0, values, 0, mArrayLength); + public boolean getValues(long[] values, int state) { + long[] counts = mStates[state].mCounter; + boolean allZeros = true; + for (int i = 0; i < counts.length; i++) { + if (counts[i] != 0) { + allZeros = false; + break; + } + } + if (allZeros) { + return false; + } + System.arraycopy(counts, 0, values, 0, mArrayLength); + return true; } public void reset() { @@ -304,7 +319,8 @@ class LongArrayMultiStateCounter_ravenwood { getInstance(targetInstanceId).copyStatesFrom(getInstance(sourceInstanceId)); } - public static void native_incrementValues(long instanceId, long[] delta, long timestampMs) { + public static void native_incrementValues(long instanceId, @Nullable long[] delta, + long timestampMs) { getInstance(instanceId).incrementValues(delta, timestampMs); } @@ -312,8 +328,8 @@ class LongArrayMultiStateCounter_ravenwood { getInstance(instanceId).addCounts(counts); } - public static void native_getCounts(long instanceId, long[] counts, int state) { - getInstance(instanceId).getValues(counts, state); + public static boolean native_getCounts(long instanceId, long[] counts, int state) { + return getInstance(instanceId).getValues(counts, state); } public static void native_reset(long instanceId) { diff --git a/core/java/com/android/internal/os/PowerStats.java b/core/java/com/android/internal/os/PowerStats.java index aafef6c8b5fd..6c69e2c623ad 100644 --- a/core/java/com/android/internal/os/PowerStats.java +++ b/core/java/com/android/internal/os/PowerStats.java @@ -128,6 +128,7 @@ public final class PowerStats { * Extra parameters specific to the power component, e.g. the availability of power * monitors. */ + @NonNull public final PersistableBundle extras; private PowerStatsFormatter mDeviceStatsFormatter; @@ -269,20 +270,41 @@ public final class PowerStats { stateStatsArrayLength, uidStatsArrayLength, extras); } + @SuppressWarnings("deprecation") @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Descriptor)) return false; Descriptor that = (Descriptor) o; - return powerComponentId == that.powerComponentId - && statsArrayLength == that.statsArrayLength - && stateLabels.contentEquals(that.stateLabels) - && stateStatsArrayLength == that.stateStatsArrayLength - && uidStatsArrayLength == that.uidStatsArrayLength - && Objects.equals(name, that.name) - && extras.size() == that.extras.size() // Unparcel the Parcel if not yet - && Bundle.kindofEquals(extras, - that.extras); // Since the Parcel is now unparceled, do a deep comparison + if (powerComponentId != that.powerComponentId + || statsArrayLength != that.statsArrayLength + || !stateLabels.contentEquals(that.stateLabels) + || stateStatsArrayLength != that.stateStatsArrayLength + || uidStatsArrayLength != that.uidStatsArrayLength + || !Objects.equals(name, that.name)) { + return false; + } + + // Getting the size has the side-effect of unparceling the Bundle if not yet + if (extras.size() != that.extras.size()) { + return false; + } + + if (Bundle.kindofEquals(extras, that.extras)) { + return true; + } + + // Since `kindofEquals` does not deep-compare arrays, we do that separately, albeit at + // the expense of creating an iterator and using a deprecated API, `bundle.get`. + // There is no performance concern, because the situation where PowerStatsDescriptors + // are changed in an incompatible way are exceedingly rare, occurring at most + // once per power component after a system upgrade. + for (String key : extras.keySet()) { + if (!Objects.deepEquals(extras.get(key), that.extras.get(key))) { + return false; + } + } + return true; } /** diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java index b57acf3d97fd..b19967a47001 100644 --- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -261,7 +261,12 @@ public class AconfigFlags { // Default value is false for when the flag is not found. // Note: Unlike with the old storage, with AconfigPackage, we don't have a way to // know if the flag is not found or if it's found but the value is false. - value = aconfigPackage.getBooleanFlagValue(flagName, false); + try { + value = aconfigPackage.getBooleanFlagValue(flagName, false); + } catch (Exception e) { + Slog.e(LOG_TAG, "Failed to read Aconfig flag value for " + flagPackageAndName, e); + return null; + } } if (DEBUG) { Slog.v(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value); diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java index 7ee22f30ace0..69c04807c604 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java @@ -157,7 +157,7 @@ public abstract class ParsedComponentImpl implements ParsedComponent, Parcelable @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(this.name); + sForInternedString.parcel(this.name, dest, flags); dest.writeInt(this.getIcon()); dest.writeInt(this.getLabelRes()); dest.writeCharSequence(this.getNonLocalizedLabel()); @@ -175,7 +175,7 @@ public abstract class ParsedComponentImpl implements ParsedComponent, Parcelable // We use the boot classloader for all classes that we load. final ClassLoader boot = Object.class.getClassLoader(); //noinspection ConstantConditions - this.name = in.readString(); + this.name = sForInternedString.unparcel(in); this.icon = in.readInt(); this.labelRes = in.readInt(); this.nonLocalizedLabel = in.readCharSequence(); diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 93be3b02a12a..2d989943800e 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -322,7 +322,7 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen PrintWriter pw = shell.getOutPrintWriter(); if (android.tracing.Flags.clientSideProtoLogging()) { - pw.println("Command deprecated. Please use 'cmd protolog' instead."); + pw.println("Command deprecated. Please use 'cmd protolog_configuration' instead."); return -1; } diff --git a/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java index 82d8d3431a9d..6d4a40899a65 100644 --- a/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java +++ b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java @@ -145,11 +145,11 @@ public class ProtoLogCommandHandler extends ShellCommand { switch (cmd) { case "enable" -> { - mProtoLogConfigurationService.enableProtoLogToLogcat(processGroups()); + mProtoLogConfigurationService.enableProtoLogToLogcat(pw, processGroups()); return 0; } case "disable" -> { - mProtoLogConfigurationService.disableProtoLogToLogcat(processGroups()); + mProtoLogConfigurationService.disableProtoLogToLogcat(pw, processGroups()); return 0; } default -> { diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java index d65aaae7deaa..a19690bbd0e4 100644 --- a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java +++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java @@ -18,6 +18,8 @@ package com.android.internal.protolog; import android.annotation.NonNull; +import java.io.PrintWriter; + public interface ProtoLogConfigurationService extends IProtoLogConfigurationService { /** * Get the list of groups clients have registered to the protolog service. @@ -37,11 +39,11 @@ public interface ProtoLogConfigurationService extends IProtoLogConfigurationServ * Enable logging target groups to logcat. * @param groups we want to enable logging them to logcat for. */ - void enableProtoLogToLogcat(@NonNull String... groups); + void enableProtoLogToLogcat(@NonNull PrintWriter pw, @NonNull String... groups); /** * Disable logging target groups to logcat. * @param groups we want to disable from being logged to logcat. */ - void disableProtoLogToLogcat(@NonNull String... groups); + void disableProtoLogToLogcat(@NonNull PrintWriter pw, @NonNull String... groups); } diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java index f83359dddfcc..ac1022ff1e0f 100644 --- a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java @@ -44,6 +44,7 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.PrintWriter; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -183,8 +184,8 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ * @param groups we want to enable logging them to logcat for. */ @Override - public void enableProtoLogToLogcat(@NonNull String... groups) { - toggleProtoLogToLogcat(true, groups); + public void enableProtoLogToLogcat(@NonNull PrintWriter pw, @NonNull String... groups) { + toggleProtoLogToLogcat(pw, true, groups); } /** @@ -192,8 +193,8 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ * @param groups we want to disable from being logged to logcat. */ @Override - public void disableProtoLogToLogcat(@NonNull String... groups) { - toggleProtoLogToLogcat(false, groups); + public void disableProtoLogToLogcat(@NonNull PrintWriter pw, @NonNull String... groups) { + toggleProtoLogToLogcat(pw, false, groups); } /** @@ -249,7 +250,9 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ } } - private void toggleProtoLogToLogcat(boolean enabled, @NonNull String[] groups) { + private void toggleProtoLogToLogcat( + @NonNull PrintWriter pw, boolean enabled, @NonNull String[] groups + ) { final var clientToGroups = new HashMap<IProtoLogClient, Set<String>>(); for (String group : groups) { @@ -257,8 +260,10 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ if (clients == null) { // No clients associated to this group - Log.w(LOG_TAG, "Attempting to toggle log to logcat for group " + group - + " with no registered clients."); + var warning = "Attempting to toggle log to logcat for group " + group + + " with no registered clients. This is a no-op."; + Log.w(LOG_TAG, warning); + pw.println("WARNING: " + warning); continue; } @@ -270,8 +275,14 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ for (IProtoLogClient client : clientToGroups.keySet()) { try { - client.toggleLogcat(enabled, clientToGroups.get(client).toArray(new String[0])); + final var clientGroups = clientToGroups.get(client).toArray(new String[0]); + pw.println("Toggling logcat logging for client " + client.toString() + + " to " + enabled + " for groups: [" + + String.join(", ", clientGroups) + "]"); + client.toggleLogcat(enabled, clientGroups); + pw.println("- Done"); } catch (RemoteException e) { + pw.println("- Failed"); throw new RuntimeException( "Failed to toggle logcat status for groups on client", e); } diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 641ecc9b675a..ce46da12aa76 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -250,7 +250,8 @@ public class ConversationLayout extends FrameLayout mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden); mPeopleHelper.animateViewForceHidden(mIcon, forceHidden); }); - mConversationText = findViewById(R.id.conversation_text); + mConversationText = findViewById(notificationsRedesignTemplates() + ? R.id.title : R.id.conversation_text); mExpandButtonContainer = findViewById(R.id.expand_button_container); mExpandButtonContainerA11yContainer = findViewById(R.id.expand_button_a11y_container); @@ -716,17 +717,10 @@ public class ConversationLayout extends FrameLayout } private void updateImageMessages() { - View newMessage = null; - if (mIsCollapsed && !mGroups.isEmpty()) { - - // When collapsed, we're displaying the image message in a dedicated container - // on the right of the layout instead of inline. Let's add the isolated image there - MessagingGroup messagingGroup = mGroups.getLast(); - MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage(); - if (isolatedMessage != null) { - newMessage = isolatedMessage.getView(); - } + if (mImageMessageContainer == null) { + return; } + View newMessage = getNewImageMessage(); // Remove all messages that don't belong into the image layout View previousMessage = mImageMessageContainer.getChildAt(0); if (previousMessage != newMessage) { @@ -738,6 +732,20 @@ public class ConversationLayout extends FrameLayout mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE); } + @Nullable + private View getNewImageMessage() { + if (mIsCollapsed && !mGroups.isEmpty()) { + // When collapsed, we're displaying the image message in a dedicated container + // on the right of the layout instead of inline. Let's add the isolated image there + MessagingGroup messagingGroup = mGroups.getLast(); + MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage(); + if (isolatedMessage != null) { + return isolatedMessage.getView(); + } + } + return null; + } + public void bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView) { applyNotificationBackgroundColor(bottomBackground); // Let's find the two last conversations: @@ -841,6 +849,10 @@ public class ConversationLayout extends FrameLayout } private void updateAppName() { + if (notificationsRedesignTemplates()) { + return; + } + mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE); } @@ -1533,6 +1545,10 @@ public class ConversationLayout extends FrameLayout } private void updateExpandButton() { + if (notificationsRedesignTemplates()) { + return; + } + int buttonGravity; ViewGroup newContainer; if (mIsCollapsed) { @@ -1565,6 +1581,10 @@ public class ConversationLayout extends FrameLayout } private void updateContentEndPaddings() { + if (notificationsRedesignTemplates()) { + return; + } + // Let's make sure the conversation header can't run into the expand button when we're // collapsed and update the paddings of the content int headerPaddingEnd; @@ -1593,6 +1613,10 @@ public class ConversationLayout extends FrameLayout } private void onAppNameVisibilityChanged() { + if (notificationsRedesignTemplates()) { + return; + } + boolean appNameGone = mAppName.getVisibility() == GONE; if (appNameGone != mAppNameGone) { mAppNameGone = appNameGone; @@ -1601,10 +1625,18 @@ public class ConversationLayout extends FrameLayout } private void updateAppNameDividerVisibility() { + if (notificationsRedesignTemplates()) { + return; + } + mAppNameDivider.setVisibility(mAppNameGone ? GONE : VISIBLE); } public void updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener) { + if (notificationsRedesignTemplates()) { + return; + } + mExpandable = expandable; if (expandable) { mExpandButtonContainer.setVisibility(VISIBLE); diff --git a/core/java/com/android/internal/widget/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java index 31d9770f6ac4..b9a603cc5696 100644 --- a/core/java/com/android/internal/widget/MessagingGroup.java +++ b/core/java/com/android/internal/widget/MessagingGroup.java @@ -449,12 +449,8 @@ public class MessagingGroup extends NotificationOptimizedLinearLayout implements } private void updateIconVisibility() { - if (Flags.notificationsRedesignTemplates() && !mIsInConversation) { - // We don't show any icon (other than the app icon) in the collapsed form. For - // conversations, keeping this container helps with aligning the message to the icon - // when collapsed, but the old messaging style already has this alignment built into - // the template like all other layouts. Conversations are special because we use the - // same base layout for both the collapsed and expanded views. + if (Flags.notificationsRedesignTemplates()) { + // We don't show any icon (other than the app or person icon) in the collapsed form. mMessagingIconContainer.setVisibility(mSingleLine ? GONE : VISIBLE); } } diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java index e9d920ca3fcd..eb22e7c8cdc0 100644 --- a/core/java/com/android/internal/widget/MessagingLayout.java +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -121,22 +121,38 @@ public class MessagingLayout extends FrameLayout setMessagingClippingDisabled(false); } - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setAvatarReplacementAsync") public void setAvatarReplacement(Icon icon) { mAvatarReplacement = icon; } - @RemotableViewMethod + /** + * @hide + */ + public Runnable setAvatarReplacementAsync(Icon icon) { + mAvatarReplacement = icon; + return () -> {}; + } + + @RemotableViewMethod(asyncImpl = "setNameReplacementAsync") public void setNameReplacement(CharSequence nameReplacement) { mNameReplacement = nameReplacement; } /** + * @hide + */ + public Runnable setNameReplacementAsync(CharSequence nameReplacement) { + mNameReplacement = nameReplacement; + return () -> {}; + } + + /** * Set this layout to show the collapsed representation. * * @param isCollapsed is it collapsed */ - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") public void setIsCollapsed(boolean isCollapsed) { mIsCollapsed = isCollapsed; } @@ -145,7 +161,6 @@ public class MessagingLayout extends FrameLayout * setDataAsync needs to do different stuff for the collapsed vs expanded view, so store the * collapsed state early. */ - @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") public Runnable setIsCollapsedAsync(boolean isCollapsed) { mIsCollapsed = isCollapsed; return () -> {}; @@ -161,12 +176,20 @@ public class MessagingLayout extends FrameLayout * * @param conversationTitle the conversation title */ - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setConversationTitleAsync") public void setConversationTitle(CharSequence conversationTitle) { mConversationTitle = conversationTitle; } /** + * @hide + */ + public Runnable setConversationTitleAsync(CharSequence conversationTitle) { + mConversationTitle = conversationTitle; + return ()->{}; + } + + /** * Set Messaging data * @param extras Bundle contains messaging data */ @@ -314,19 +337,10 @@ public class MessagingLayout extends FrameLayout } private void updateImageMessages() { - View newMessage = null; if (mImageMessageContainer == null) { return; } - if (mIsCollapsed && !mGroups.isEmpty()) { - // When collapsed, we're displaying the image message in a dedicated container - // on the right of the layout instead of inline. Let's add the isolated image there - MessagingGroup messagingGroup = mGroups.getLast(); - MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage(); - if (isolatedMessage != null) { - newMessage = isolatedMessage.getView(); - } - } + View newMessage = getNewImageMessage(); // Remove all messages that don't belong into the image layout View previousMessage = mImageMessageContainer.getChildAt(0); if (previousMessage != newMessage) { @@ -345,6 +359,20 @@ public class MessagingLayout extends FrameLayout } } + @Nullable + private View getNewImageMessage() { + if (mIsCollapsed && !mGroups.isEmpty()) { + // When collapsed, we're displaying the image message in a dedicated container + // on the right of the layout instead of inline. Let's add the isolated image there + MessagingGroup messagingGroup = mGroups.getLast(); + MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage(); + if (isolatedMessage != null) { + return isolatedMessage.getView(); + } + } + return null; + } + private void removeGroups(ArrayList<MessagingGroup> oldGroups) { int size = oldGroups.size(); for (int i = 0; i < size; i++) { @@ -417,22 +445,44 @@ public class MessagingLayout extends FrameLayout return mPeopleHelper.createAvatarSymbol(senderName, symbol, layoutColor); } - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setLayoutColorAsync") public void setLayoutColor(int color) { mLayoutColor = color; } - @RemotableViewMethod + /** + * @hide + */ + public Runnable setLayoutColorAsync(int color) { + mLayoutColor = color; + return () -> {}; + } + + @RemotableViewMethod(asyncImpl = "setIsOneToOneAsync") public void setIsOneToOne(boolean oneToOne) { mIsOneToOne = oneToOne; } - @RemotableViewMethod + /** + * @hide + */ + public Runnable setIsOneToOneAsync(boolean oneToOne) { + mIsOneToOne = oneToOne; + return () -> {}; + } + + @RemotableViewMethod(asyncImpl = "setSenderTextColorAsync") public void setSenderTextColor(int color) { mSenderTextColor = color; } - + /** + * @hide + */ + public Runnable setSenderTextColorAsync(int color) { + mSenderTextColor = color; + return () -> {}; + } /** * @param color the color of the notification background */ @@ -441,11 +491,19 @@ public class MessagingLayout extends FrameLayout // Nothing to do with this } - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setMessageTextColorAsync") public void setMessageTextColor(int color) { mMessageTextColor = color; } + /** + * @hide + */ + public Runnable setMessageTextColorAsync(int color) { + mMessageTextColor = color; + return () -> {}; + } + public void setUser(Person user) { mUser = user; if (mUser.getIcon() == null) { diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 0c18de92a391..92a841f9c290 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -76,6 +76,9 @@ cc_library_shared_for_libandroid_runtime { srcs: [ "android_animation_PropertyValuesHolder.cpp", "android_content_res_ApkAssets.cpp", + "android_media_ImageReader.cpp", + "android_media_PublicFormatUtils.cpp", + "android_media_Utils.cpp", "android_os_SystemClock.cpp", "android_os_SystemProperties.cpp", "android_text_AndroidCharacter.cpp", @@ -134,10 +137,7 @@ cc_library_shared_for_libandroid_runtime { "android_app_ActivityThread.cpp", "android_app_NativeActivity.cpp", "android_app_admin_SecurityLog.cpp", - "android_media_ImageReader.cpp", "android_media_ImageWriter.cpp", - "android_media_PublicFormatUtils.cpp", - "android_media_Utils.cpp", "android_opengl_EGL14.cpp", "android_opengl_EGL15.cpp", "android_opengl_EGLExt.cpp", diff --git a/core/jni/android_app_PropertyInvalidatedCache.cpp b/core/jni/android_app_PropertyInvalidatedCache.cpp index 12585d5f8137..6267522f114f 100644 --- a/core/jni/android_app_PropertyInvalidatedCache.cpp +++ b/core/jni/android_app_PropertyInvalidatedCache.cpp @@ -35,7 +35,7 @@ int NonceStore::getMaxNonce() const { return kMaxNonce; } -size_t NonceStore::getMaxByte() const { +int32_t NonceStore::getMaxByte() const { return kMaxByte; } @@ -68,13 +68,13 @@ int32_t NonceStore::getHash() const { } // Copy the byte block to the target and return the current hash. -int32_t NonceStore::getByteBlock(block_t* block, size_t len) const { +int32_t NonceStore::getByteBlock(block_t* block, int32_t len) const { memcpy(block, (void*) byteBlock(), std::min(kMaxByte, len)); return mByteHash; } // Set the byte block and the hash. -void NonceStore::setByteBlock(int hash, const block_t* block, size_t len) { +void NonceStore::setByteBlock(int hash, const block_t* block, int32_t len) { memcpy((void*) byteBlock(), block, len = std::min(kMaxByte, len)); mByteHash = hash; } diff --git a/core/jni/android_app_PropertyInvalidatedCache.h b/core/jni/android_app_PropertyInvalidatedCache.h index 54a4ac65fce2..1d75182356c6 100644 --- a/core/jni/android_app_PropertyInvalidatedCache.h +++ b/core/jni/android_app_PropertyInvalidatedCache.h @@ -27,8 +27,12 @@ namespace android::app::PropertyInvalidatedCache { * location. Fields with a variable location are found via offsets. The offsets make this * object position-independent, which is required because it is in shared memory and would be * mapped into different virtual addresses for different processes. + * + * This structure is shared between 64-bit and 32-bit processes. Therefore it is imperative + * that the structure not use any datatypes that are architecture-dependent (like size_t). + * Additionally, care must be taken to avoid unexpected padding in the structure. */ -class NonceStore { +class alignas(8) NonceStore { protected: // A convenient typedef. The jbyteArray element type is jbyte, which the compiler treats as // signed char. @@ -43,23 +47,27 @@ class NonceStore { // The value of an unset field. static constexpr int UNSET = 0; - // The size of the nonce array. + // The size of the nonce array. This and the following sizes are int32_t to + // be ABI independent. const int32_t kMaxNonce; // The size of the byte array. - const size_t kMaxByte; + const int32_t kMaxByte; // The offset to the nonce array. - const size_t mNonceOffset; + const int32_t mNonceOffset; // The offset to the byte array. - const size_t mByteOffset; + const int32_t mByteOffset; // The byte block hash. This is fixed and at a known offset, so leave it in the base class. volatile std::atomic<int32_t> mByteHash; + // A 4-byte padd that makes the size of this structure a multiple of 8 bytes. + const int32_t _pad = 0; + // The constructor is protected! It only makes sense when called from a subclass. - NonceStore(int kMaxNonce, size_t kMaxByte, volatile nonce_t* nonce, volatile block_t* block) : + NonceStore(int kMaxNonce, int kMaxByte, volatile nonce_t* nonce, volatile block_t* block) : kMaxNonce(kMaxNonce), kMaxByte(kMaxByte), mNonceOffset(offset(this, const_cast<nonce_t*>(nonce))), @@ -70,7 +78,7 @@ class NonceStore { // These provide run-time access to the sizing parameters. int getMaxNonce() const; - size_t getMaxByte() const; + int getMaxByte() const; // Fetch a nonce, returning UNSET if the index is out of range. This method specifically // does not throw or generate an error if the index is out of range; this allows the method @@ -86,10 +94,10 @@ class NonceStore { int32_t getHash() const; // Copy the byte block to the target and return the current hash. - int32_t getByteBlock(block_t* block, size_t len) const; + int32_t getByteBlock(block_t* block, int32_t len) const; // Set the byte block and the hash. - void setByteBlock(int hash, const block_t* block, size_t len); + void setByteBlock(int hash, const block_t* block, int32_t len); private: @@ -113,6 +121,12 @@ class NonceStore { } }; +// Assert that the size of the object is fixed, independent of the CPU architecture. There are +// four int32_t fields and one atomic<int32_t>, which sums to 20 bytes total. This assertion +// uses a constant instead of computing the size of the objects in the compiler, to avoid +// different answers on different architectures. +static_assert(sizeof(NonceStore) == 24); + /** * A cache nonce block contains an array of std::atomic<int64_t> and an array of bytes. The * byte array has an associated hash. This class provides methods to read and write the fields @@ -126,20 +140,22 @@ class NonceStore { * The template is parameterized by the number of nonces it supports and the number of bytes in * the string block. */ -template<int maxNonce, size_t maxByte> class CacheNonce : public NonceStore { +template<int MAX_NONCE, int MAX_BYTE> class CacheNonce : public NonceStore { // The array of nonces - volatile nonce_t mNonce[maxNonce]; + volatile nonce_t mNonce[MAX_NONCE]; // The byte array. This is not atomic but it is guarded by the mByteHash. - volatile block_t mByteBlock[maxByte]; + volatile block_t mByteBlock[MAX_BYTE]; public: + // Export the parameters for use in compiler assertions. + static constexpr int kMaxNonceCount = MAX_NONCE; + static constexpr int kMaxByteCount = MAX_BYTE; + // Construct and initialize the memory. - CacheNonce() : - NonceStore(maxNonce, maxByte, &mNonce[0], &mByteBlock[0]) - { - for (int i = 0; i < maxNonce; i++) { + CacheNonce() : NonceStore(MAX_NONCE, MAX_BYTE, &mNonce[0], &mByteBlock[0]) { + for (int i = 0; i < MAX_NONCE; i++) { mNonce[i] = UNSET; } mByteHash = UNSET; @@ -155,4 +171,10 @@ template<int maxNonce, size_t maxByte> class CacheNonce : public NonceStore { typedef CacheNonce</* max nonce */ 128, /* byte block size */ 8192> SystemCacheNonce; // LINT.ThenChange(/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java:system_nonce_config) +// Verify that there is no padding in the final class. +static_assert(sizeof(SystemCacheNonce) == + sizeof(NonceStore) + + SystemCacheNonce::kMaxNonceCount*8 + + SystemCacheNonce::kMaxByteCount); + } // namespace android.app.PropertyInvalidatedCache diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp index a52678359423..1a7490e61873 100644 --- a/core/jni/android_graphics_BLASTBufferQueue.cpp +++ b/core/jni/android_graphics_BLASTBufferQueue.cpp @@ -26,6 +26,7 @@ #include <nativehelper/JNIHelp.h> #include <utils/Log.h> #include <utils/RefBase.h> +#include <utils/StrongPointer.h> #include "core_jni_helpers.h" @@ -124,7 +125,7 @@ private: static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring jName, jboolean updateDestinationFrame) { ScopedUtfChars name(env, jName); - sp<BLASTBufferQueue> queue = new BLASTBufferQueue(name.c_str(), updateDestinationFrame); + sp<BLASTBufferQueue> queue = sp<BLASTBufferQueue>::make(name.c_str(), updateDestinationFrame); queue->incStrong((void*)nativeCreate); return reinterpret_cast<jlong>(queue.get()); } diff --git a/core/jni/android_media_ImageReader.cpp b/core/jni/android_media_ImageReader.cpp index 20b9c571317e..a34cb391c2fd 100644 --- a/core/jni/android_media_ImageReader.cpp +++ b/core/jni/android_media_ImageReader.cpp @@ -25,13 +25,22 @@ #include <android_runtime/android_view_Surface.h> #include <com_android_graphics_libgui_flags.h> #include <cutils/atomic.h> +#ifdef __ANDROID__ #include <grallocusage/GrallocUsageConversion.h> +#else +#define GRALLOC_USAGE_PROTECTED 0 +#define GRALLOC_USAGE_SW_READ_OFTEN 0 +#define GRALLOC_USAGE_SW_WRITE_OFTEN 0 +#endif #include <gui/BufferItemConsumer.h> #include <gui/Surface.h> #include <inttypes.h> #include <jni.h> +#include <jni_wrappers.h> #include <nativehelper/JNIHelp.h> +#ifdef __ANDROID__ #include <private/android/AHardwareBufferHelpers.h> +#endif #include <stdint.h> #include <ui/Rect.h> #include <utils/List.h> @@ -393,8 +402,12 @@ static void ImageReader_init(JNIEnv* env, jobject thiz, jobject weakThiz, jint w String8 consumerName = String8::format("ImageReader-%dx%df%xm%d-%d-%d", width, height, nativeHalFormat, maxImages, getpid(), createProcessUniqueId()); +#ifdef __ANDROID__ uint64_t consumerUsage = android_hardware_HardwareBuffer_convertToGrallocUsageBits(ndkUsage); +#else + uint64_t consumerUsage = 0; +#endif #if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) sp<BufferItemConsumer> bufferConsumer = new BufferItemConsumer(consumerUsage, maxImages, @@ -773,6 +786,7 @@ static bool Image_getLockedImageInfo(JNIEnv* env, LockedImage* buffer, int idx, return true; } +#ifdef __ANDROID__ static void ImageReader_unlockGraphicBuffer(JNIEnv* env, jobject /*thiz*/, jobject buffer) { sp<GraphicBuffer> graphicBuffer = @@ -856,6 +870,7 @@ static jobjectArray ImageReader_createImagePlanes(JNIEnv* env, jobject /*thiz*/, return imagePlanes; } +#endif static jobjectArray Image_createSurfacePlanes(JNIEnv* env, jobject thiz, int numPlanes, int halReaderFormat, uint64_t ndkReaderUsage) @@ -964,6 +979,7 @@ static jint Image_getFormat(JNIEnv* env, jobject thiz, jint readerFormat) } } +#ifdef __ANDROID__ static jobject Image_getHardwareBuffer(JNIEnv* env, jobject thiz) { BufferItem* buffer = Image_getBufferItem(env, thiz); if (buffer == nullptr) { @@ -976,45 +992,48 @@ static jobject Image_getHardwareBuffer(JNIEnv* env, jobject thiz) { // to link against libandroid.so return android_hardware_HardwareBuffer_createFromAHardwareBuffer(env, b); } +#endif } // extern "C" // ---------------------------------------------------------------------------- -static const JNINativeMethod gImageReaderMethods[] = { - {"nativeClassInit", "()V", (void*)ImageReader_classInit }, - {"nativeInit", "(Ljava/lang/Object;IIIJII)V", (void*)ImageReader_init }, - {"nativeClose", "()V", (void*)ImageReader_close }, - {"nativeReleaseImage", "(Landroid/media/Image;)V", (void*)ImageReader_imageRelease }, - {"nativeImageSetup", "(Landroid/media/Image;)I", (void*)ImageReader_imageSetup }, - {"nativeGetSurface", "()Landroid/view/Surface;", (void*)ImageReader_getSurface }, - {"nativeDetachImage", "(Landroid/media/Image;Z)I", (void*)ImageReader_detachImage }, - {"nativeCreateImagePlanes", - "(ILandroid/graphics/GraphicBuffer;IIIIII)[Landroid/media/ImageReader$ImagePlane;", - (void*)ImageReader_createImagePlanes }, - {"nativeUnlockGraphicBuffer", - "(Landroid/graphics/GraphicBuffer;)V", (void*)ImageReader_unlockGraphicBuffer }, - {"nativeDiscardFreeBuffers", "()V", (void*)ImageReader_discardFreeBuffers } -}; +static const JNINativeMethod gImageReaderMethods[] = + {{"nativeClassInit", "()V", (void*)ImageReader_classInit}, + {"nativeInit", "(Ljava/lang/Object;IIIJII)V", (void*)ImageReader_init}, + {"nativeClose", "()V", (void*)ImageReader_close}, + {"nativeReleaseImage", "(Landroid/media/Image;)V", (void*)ImageReader_imageRelease}, + {"nativeImageSetup", "(Landroid/media/Image;)I", (void*)ImageReader_imageSetup}, + {"nativeGetSurface", "()Landroid/view/Surface;", (void*)ImageReader_getSurface}, + {"nativeDetachImage", "(Landroid/media/Image;Z)I", (void*)ImageReader_detachImage}, +#ifdef __ANDROID__ + {"nativeCreateImagePlanes", + "(ILandroid/graphics/GraphicBuffer;IIIIII)[Landroid/media/ImageReader$ImagePlane;", + (void*)ImageReader_createImagePlanes}, + {"nativeUnlockGraphicBuffer", "(Landroid/graphics/GraphicBuffer;)V", + (void*)ImageReader_unlockGraphicBuffer}, +#endif + {"nativeDiscardFreeBuffers", "()V", (void*)ImageReader_discardFreeBuffers}}; static const JNINativeMethod gImageMethods[] = { - {"nativeCreatePlanes", "(IIJ)[Landroid/media/ImageReader$SurfaceImage$SurfacePlane;", - (void*)Image_createSurfacePlanes }, - {"nativeGetWidth", "()I", (void*)Image_getWidth }, - {"nativeGetHeight", "()I", (void*)Image_getHeight }, - {"nativeGetFormat", "(I)I", (void*)Image_getFormat }, - {"nativeGetFenceFd", "()I", (void*)Image_getFenceFd }, - {"nativeGetHardwareBuffer", "()Landroid/hardware/HardwareBuffer;", - (void*)Image_getHardwareBuffer }, + {"nativeCreatePlanes", "(IIJ)[Landroid/media/ImageReader$SurfaceImage$SurfacePlane;", + (void*)Image_createSurfacePlanes}, + {"nativeGetWidth", "()I", (void*)Image_getWidth}, + {"nativeGetHeight", "()I", (void*)Image_getHeight}, + {"nativeGetFormat", "(I)I", (void*)Image_getFormat}, + {"nativeGetFenceFd", "()I", (void*)Image_getFenceFd}, +#ifdef __ANDROID__ + {"nativeGetHardwareBuffer", "()Landroid/hardware/HardwareBuffer;", + (void*)Image_getHardwareBuffer}, +#endif }; int register_android_media_ImageReader(JNIEnv *env) { + int ret1 = RegisterMethodsOrDie(env, "android/media/ImageReader", gImageReaderMethods, + NELEM(gImageReaderMethods)); - int ret1 = AndroidRuntime::registerNativeMethods(env, - "android/media/ImageReader", gImageReaderMethods, NELEM(gImageReaderMethods)); - - int ret2 = AndroidRuntime::registerNativeMethods(env, - "android/media/ImageReader$SurfaceImage", gImageMethods, NELEM(gImageMethods)); + int ret2 = RegisterMethodsOrDie(env, "android/media/ImageReader$SurfaceImage", gImageMethods, + NELEM(gImageMethods)); return (ret1 || ret2); } diff --git a/core/jni/android_media_PublicFormatUtils.cpp b/core/jni/android_media_PublicFormatUtils.cpp index 04494ad00a65..bcdf654d5a8c 100644 --- a/core/jni/android_media_PublicFormatUtils.cpp +++ b/core/jni/android_media_PublicFormatUtils.cpp @@ -16,10 +16,10 @@ #define LOG_TAG "PublicFormatUtils_JNI" -#include <utils/misc.h> -#include <ui/PublicFormat.h> -#include <android_runtime/AndroidRuntime.h> #include <jni.h> +#include <jni_wrappers.h> +#include <ui/PublicFormat.h> +#include <utils/misc.h> using namespace android; @@ -53,7 +53,6 @@ static const JNINativeMethod gMethods[] = { }; int register_android_media_PublicFormatUtils(JNIEnv *env) { - return AndroidRuntime::registerNativeMethods(env, - "android/media/PublicFormatUtils", gMethods, NELEM(gMethods)); + return RegisterMethodsOrDie(env, "android/media/PublicFormatUtils", gMethods, NELEM(gMethods)); } diff --git a/core/jni/android_media_Utils.cpp b/core/jni/android_media_Utils.cpp index e8f8644a4503..e48414113cf0 100644 --- a/core/jni/android_media_Utils.cpp +++ b/core/jni/android_media_Utils.cpp @@ -19,10 +19,12 @@ #include "android_media_Utils.h" +#ifdef __ANDROID__ // Layoutlib does not support hardware #include <aidl/android/hardware/graphics/common/PixelFormat.h> #include <aidl/android/hardware/graphics/common/PlaneLayoutComponentType.h> #include <ui/GraphicBufferMapper.h> #include <ui/GraphicTypes.h> +#endif #include <utils/Log.h> #define ALIGN(x, mask) ( ((x) + (mask) - 1) & ~((mask) - 1) ) @@ -34,7 +36,13 @@ namespace android { // -----------Utility functions used by ImageReader/Writer JNI----------------- +#ifdef __ANDROID__ using AidlPixelFormat = aidl::android::hardware::graphics::common::PixelFormat; +#else +namespace AidlPixelFormat { +const int32_t YCBCR_P210 = 60; +} +#endif enum { IMAGE_MAX_NUM_PLANES = 3, @@ -517,6 +525,7 @@ status_t getLockedImageInfo(LockedImage* buffer, int idx, return OK; } +#ifdef __ANDROID__ static status_t extractP010Gralloc4PlaneLayout( sp<GraphicBuffer> buffer, void *pData, int format, LockedImage *outputImage) { using aidl::android::hardware::graphics::common::PlaneLayoutComponent; @@ -663,6 +672,7 @@ static status_t extractP210Gralloc4PlaneLayout(sp<GraphicBuffer> buffer, void *p outputImage->chromaStep = 4; return OK; } +#endif status_t lockImageFromBuffer(sp<GraphicBuffer> buffer, uint32_t inUsage, const Rect& rect, int fenceFd, LockedImage* outputImage) { @@ -701,6 +711,7 @@ status_t lockImageFromBuffer(sp<GraphicBuffer> buffer, uint32_t inUsage, ALOGE("Lock buffer failed!"); return res; } +#ifdef __ANDROID__ if (isPossibly10BitYUV(format)) { if (format == HAL_PIXEL_FORMAT_YCBCR_P010 && OK == extractP010Gralloc4PlaneLayout(buffer, pData, format, outputImage)) { @@ -713,6 +724,7 @@ status_t lockImageFromBuffer(sp<GraphicBuffer> buffer, uint32_t inUsage, return OK; } } +#endif } outputImage->data = reinterpret_cast<uint8_t*>(pData); diff --git a/core/jni/android_os_PerfettoTrackEventExtra.cpp b/core/jni/android_os_PerfettoTrackEventExtra.cpp index b8bdc8c29199..18f05cabd12d 100644 --- a/core/jni/android_os_PerfettoTrackEventExtra.cpp +++ b/core/jni/android_os_PerfettoTrackEventExtra.cpp @@ -23,7 +23,8 @@ #include <nativehelper/utils.h> #include <tracing_sdk.h> -static constexpr ssize_t kMaxStrLen = 4096; +#include <list> + namespace android { template <typename T> inline static T* toPointer(jlong ptr) { @@ -35,24 +36,145 @@ inline static jlong toJLong(T* ptr) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(ptr)); } +/** + * @brief A thread-safe utility class for converting Java UTF-16 strings to ASCII in JNI + * environment. + * + * StringBuffer provides efficient conversion of Java strings to ASCII with optimized memory + * handling. + * It uses a two-tiered buffering strategy: + * 1. A fast path using pre-allocated thread-local buffers for strings up to 128 characters + * 2. A fallback path using dynamic allocation for longer strings + * + * Non-ASCII characters (>255) are replaced with '?' during conversion. The class maintains + * thread safety through thread-local storage and provides zero-copy string views for optimal + * performance. + * + * Memory Management: + * - Uses fixed-size thread-local buffers for both UTF-16 and ASCII characters + * - Overflow strings are stored in a thread-local list to maintain valid string views + * - Avoids unnecessary allocations in the common case of small strings + * + * Usage example: + * @code + * JNIEnv* env = ...; + * jstring java_string = ...; + * std::string_view ascii = StringBuffer::utf16_to_ascii(env, java_string); + * // Use the ASCII string... + * StringBuffer::reset(); // Clean up when done + * @endcode + * + * Thread Safety: All methods are thread-safe due to thread-local storage. + */ +class StringBuffer { +private: + static constexpr size_t BASE_SIZE = 128; + // Temporarily stores the UTF-16 characters retrieved from the Java + // string before they are converted to ASCII. + static thread_local inline char char_buffer[BASE_SIZE]; + // For fast-path conversions when the resulting ASCII string fits within + // the pre-allocated space. All ascii strings in a trace event will be stored + // here until emitted. + static thread_local inline jchar jchar_buffer[BASE_SIZE]; + // When the fast-path conversion is not possible (because char_buffer + // doesn't have enough space), the converted ASCII string is stored + // in this list. We use list here to avoid moving the strings on resize + // with vector. This way, we can give out string_views from the stored strings. + // The additional overhead from list node allocations is fine cos we are already + // in an extremely unlikely path here and there are other bigger problems if here. + static thread_local inline std::list<std::string> overflow_strings; + // current offset into the char_buffer. + static thread_local inline size_t current_offset{0}; + // This allows us avoid touching the overflow_strings directly in the fast path. + // Touching it causes some thread local init routine to run which shows up in profiles. + static thread_local inline bool is_overflow_strings_empty = true; + + static void copy_utf16_to_ascii(const jchar* src, size_t len, char* dst, JNIEnv* env, + jstring str) { + std::transform(src, src + len, dst, + [](jchar c) { return (c <= 0xFF) ? static_cast<char>(c) : '?'; }); + + if (src != jchar_buffer) { + // We hit the slow path to populate src, so we have to release. + env->ReleaseStringCritical(str, src); + } + } + +public: + static void reset() { + if (!is_overflow_strings_empty) { + overflow_strings.clear(); + is_overflow_strings_empty = true; + } + current_offset = 0; + } + + // Converts a Java string (jstring) to an ASCII string_view. Characters + // outside the ASCII range (0-255) are replaced with '?'. + // + // @param env The JNI environment. + // @param val The Java string to convert. + // @return A string_view representing the ASCII version of the string. + // Returns an empty string_view if the input is null or empty. + static std::string_view utf16_to_ascii(JNIEnv* env, jstring val) { + if (!val) return ""; + + const jsize len = env->GetStringLength(val); + if (len == 0) return ""; + + const jchar* temp_buffer; + + // Fast path: Enough space in jchar_buffer + if (static_cast<size_t>(len) <= BASE_SIZE) { + env->GetStringRegion(val, 0, len, jchar_buffer); + temp_buffer = jchar_buffer; + } else { + // Slow path: Fallback to asking ART for the string which will likely + // allocate and return a copy. + temp_buffer = env->GetStringCritical(val, nullptr); + } + + const size_t next_offset = current_offset + len + 1; + // Fast path: Enough space in char_buffer + if (BASE_SIZE > next_offset) { + const size_t start_offset = current_offset; + + copy_utf16_to_ascii(temp_buffer, len, char_buffer + current_offset, env, val); + char_buffer[current_offset + len] = '\0'; + + auto res = std::string_view(char_buffer + current_offset, len); + current_offset = next_offset; + return res; + } else { + // Slow path: Not enough space in char_buffer. Use overflow_strings. + // This will cause a string alloc but should be very unlikely to hit. + std::string& str = overflow_strings.emplace_back(len + 1, '\0'); + + copy_utf16_to_ascii(temp_buffer, len, str.data(), env, val); + is_overflow_strings_empty = false; + return std::string_view(str); + } + } +}; + static jlong android_os_PerfettoTrackEventExtraArgInt64_init(JNIEnv* env, jclass, jstring name) { - ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name); - return toJLong(new tracing_perfetto::DebugArg<int64_t>(name_chars.c_str())); + return toJLong(new tracing_perfetto::DebugArg<int64_t>( + StringBuffer::utf16_to_ascii(env, name).data())); } static jlong android_os_PerfettoTrackEventExtraArgBool_init(JNIEnv* env, jclass, jstring name) { - ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name); - return toJLong(new tracing_perfetto::DebugArg<bool>(name_chars.c_str())); + return toJLong( + new tracing_perfetto::DebugArg<bool>(StringBuffer::utf16_to_ascii(env, name).data())); } static jlong android_os_PerfettoTrackEventExtraArgDouble_init(JNIEnv* env, jclass, jstring name) { - ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name); - return toJLong(new tracing_perfetto::DebugArg<double>(name_chars.c_str())); + return toJLong( + new tracing_perfetto::DebugArg<double>(StringBuffer::utf16_to_ascii(env, name).data())); } static jlong android_os_PerfettoTrackEventExtraArgString_init(JNIEnv* env, jclass, jstring name) { - ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name); - return toJLong(new tracing_perfetto::DebugArg<const char*>(name_chars.c_str())); + return toJLong(new tracing_perfetto::DebugArg<const char*>( + StringBuffer::utf16_to_ascii(env, name).data())); } static jlong android_os_PerfettoTrackEventExtraArgInt64_delete() { @@ -109,11 +231,9 @@ static void android_os_PerfettoTrackEventExtraArgDouble_set_value(jlong ptr, jdo static void android_os_PerfettoTrackEventExtraArgString_set_value(JNIEnv* env, jclass, jlong ptr, jstring val) { - ScopedUtfChars val_chars = GET_UTF_OR_RETURN_VOID(env, val); - tracing_perfetto::DebugArg<const char*>* arg = toPointer<tracing_perfetto::DebugArg<const char*>>(ptr); - arg->set_value(strdup(val_chars.c_str())); + arg->set_value(StringBuffer::utf16_to_ascii(env, val).data()); } static jlong android_os_PerfettoTrackEventExtraFieldInt64_init() { @@ -186,11 +306,9 @@ static void android_os_PerfettoTrackEventExtraFieldDouble_set_value(jlong ptr, j static void android_os_PerfettoTrackEventExtraFieldString_set_value(JNIEnv* env, jclass, jlong ptr, jlong id, jstring val) { - ScopedUtfChars val_chars = GET_UTF_OR_RETURN_VOID(env, val); - tracing_perfetto::ProtoField<const char*>* field = toPointer<tracing_perfetto::ProtoField<const char*>>(ptr); - field->set_value(id, strdup(val_chars.c_str())); + field->set_value(id, StringBuffer::utf16_to_ascii(env, val).data()); } static void android_os_PerfettoTrackEventExtraFieldNested_add_field(jlong field_ptr, @@ -231,8 +349,9 @@ static jlong android_os_PerfettoTrackEventExtraFlow_get_extra_ptr(jlong ptr) { static jlong android_os_PerfettoTrackEventExtraNamedTrack_init(JNIEnv* env, jclass, jlong id, jstring name, jlong parent_uuid) { - ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name); - return toJLong(new tracing_perfetto::NamedTrack(id, parent_uuid, name_chars.c_str())); + return toJLong( + new tracing_perfetto::NamedTrack(id, parent_uuid, + StringBuffer::utf16_to_ascii(env, name).data())); } static jlong android_os_PerfettoTrackEventExtraNamedTrack_delete() { @@ -246,9 +365,10 @@ static jlong android_os_PerfettoTrackEventExtraNamedTrack_get_extra_ptr(jlong pt static jlong android_os_PerfettoTrackEventExtraCounterTrack_init(JNIEnv* env, jclass, jstring name, jlong parent_uuid) { - ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name); - - return toJLong(new tracing_perfetto::RegisteredTrack(1, parent_uuid, name_chars.c_str(), true)); + return toJLong( + new tracing_perfetto::RegisteredTrack(1, parent_uuid, + StringBuffer::utf16_to_ascii(env, name).data(), + true)); } static jlong android_os_PerfettoTrackEventExtraCounterTrack_delete() { @@ -318,11 +438,11 @@ static void android_os_PerfettoTrackEventExtra_clear_args(jlong ptr) { static void android_os_PerfettoTrackEventExtra_emit(JNIEnv* env, jclass, jint type, jlong cat_ptr, jstring name, jlong extra_ptr) { - ScopedUtfChars name_chars = GET_UTF_OR_RETURN_VOID(env, name); - tracing_perfetto::Category* category = toPointer<tracing_perfetto::Category>(cat_ptr); - tracing_perfetto::trace_event(type, category->get(), name_chars.c_str(), + tracing_perfetto::trace_event(type, category->get(), + StringBuffer::utf16_to_ascii(env, name).data(), toPointer<tracing_perfetto::Extra>(extra_ptr)); + StringBuffer::reset(); } static jlong android_os_PerfettoTrackEventExtraProto_init() { diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp index b2649a471b8a..a73ff421acf7 100644 --- a/core/jni/android_util_AssetManager.cpp +++ b/core/jni/android_util_AssetManager.cpp @@ -424,6 +424,15 @@ static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jin assetmanager->SetDefaultLocale(default_locale_int); } +static void NativeSetOverlayConstraints(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr, + jint displayId, jint deviceId) { + ATRACE_NAME("AssetManager::SetDisplayIdAndDeviceId"); + + auto assetmanager = LockAndStartAssetManager(ptr); + assetmanager->SetOverlayConstraints(static_cast<int32_t>(displayId), + static_cast<int32_t>(deviceId)); +} + static jobject NativeGetAssignedPackageIdentifiers(JNIEnv* env, jclass /*clazz*/, jlong ptr, jboolean includeOverlays, jboolean includeLoaders) { @@ -1554,6 +1563,7 @@ static const JNINativeMethod gAssetManagerMethods[] = { {"nativeSetApkAssets", "(J[Landroid/content/res/ApkAssets;ZZ)V", (void*)NativeSetApkAssets}, {"nativeSetConfiguration", "(JIILjava/lang/String;[Ljava/lang/String;IIIIIIIIIIIIIIIIZ)V", (void*)NativeSetConfiguration}, + {"nativeSetOverlayConstraints", "(JII)V", (void*)NativeSetOverlayConstraints}, {"nativeGetAssignedPackageIdentifiers", "(JZZ)Landroid/util/SparseArray;", (void*)NativeGetAssignedPackageIdentifiers}, diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index b99b0ef7f24e..a8e51a7c1fe8 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -765,28 +765,28 @@ static void nativeSetLuts(JNIEnv* env, jclass clazz, jlong transactionObj, jlong std::vector<int32_t> dimensions; std::vector<int32_t> sizes; std::vector<int32_t> samplingKeys; - int32_t fd = -1; + base::unique_fd fd; if (jdimensionArray) { jsize numLuts = env->GetArrayLength(jdimensionArray); - ScopedIntArrayRW joffsets(env, joffsetArray); + ScopedIntArrayRO joffsets(env, joffsetArray); if (joffsets.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from joffsetArray"); + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRO from joffsetArray"); return; } - ScopedIntArrayRW jdimensions(env, jdimensionArray); + ScopedIntArrayRO jdimensions(env, jdimensionArray); if (jdimensions.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jdimensionArray"); + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRO from jdimensionArray"); return; } - ScopedIntArrayRW jsizes(env, jsizeArray); + ScopedIntArrayRO jsizes(env, jsizeArray); if (jsizes.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsizeArray"); + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRO from jsizeArray"); return; } - ScopedIntArrayRW jsamplingKeys(env, jsamplingKeyArray); + ScopedIntArrayRO jsamplingKeys(env, jsamplingKeyArray); if (jsamplingKeys.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsamplingKeyArray"); + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRO from jsamplingKeyArray"); return; } @@ -796,15 +796,15 @@ static void nativeSetLuts(JNIEnv* env, jclass clazz, jlong transactionObj, jlong sizes = std::vector<int32_t>(jsizes.get(), jsizes.get() + numLuts); samplingKeys = std::vector<int32_t>(jsamplingKeys.get(), jsamplingKeys.get() + numLuts); - ScopedFloatArrayRW jbuffers(env, jbufferArray); + ScopedFloatArrayRO jbuffers(env, jbufferArray); if (jbuffers.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedFloatArrayRW from jbufferArray"); + jniThrowRuntimeException(env, "Failed to get ScopedFloatArrayRO from jbufferArray"); return; } // create the shared memory and copy jbuffers size_t bufferSize = jbuffers.size() * sizeof(float); - fd = ashmem_create_region("lut_shared_mem", bufferSize); + fd.reset(ashmem_create_region("lut_shared_mem", bufferSize)); if (fd < 0) { jniThrowRuntimeException(env, "ashmem_create_region() failed"); return; @@ -820,7 +820,7 @@ static void nativeSetLuts(JNIEnv* env, jclass clazz, jlong transactionObj, jlong } } - transaction->setLuts(ctrl, base::unique_fd(fd), offsets, dimensions, sizes, samplingKeys); + transaction->setLuts(ctrl, std::move(fd), offsets, dimensions, sizes, samplingKeys); } static void nativeSetPictureProfileId(JNIEnv* env, jclass clazz, jlong transactionObj, diff --git a/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp b/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp index 7ffe0ed7c6cd..7d94ef85f51a 100644 --- a/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp +++ b/core/jni/com_android_internal_os_LongArrayMultiStateCounter.cpp @@ -97,7 +97,13 @@ static void native_updateValues(JNIEnv *env, jclass, jlong nativePtr, jlongArray static void native_incrementValues(JNIEnv *env, jclass, jlong nativePtr, jlongArray values, jlong timestamp) { auto *counter = reinterpret_cast<LongArrayMultiStateCounter *>(nativePtr); - counter->incrementValue(JavaUint64Array(env, values), timestamp); + if (values != nullptr) { + counter->incrementValue(JavaUint64Array(env, values), timestamp); + } else { + // Pass an empty Uint64Array, which is equivalent to an array of zeros. + // This is done to ensure that the timestamp is still updated in the counter. + counter->incrementValue(Uint64Array(), timestamp); + } } static void native_addCounts(JNIEnv *env, jclass, jlong nativePtr, jlongArray values) { @@ -110,17 +116,26 @@ static void native_reset(jlong nativePtr) { counter->reset(); } -static void native_getCounts(JNIEnv *env, jclass, jlong nativePtr, jlongArray values, jint state) { +static bool native_getCounts(JNIEnv *env, jclass, jlong nativePtr, jlongArray values, jint state) { auto *counter = reinterpret_cast<LongArrayMultiStateCounter *>(nativePtr); - ScopedLongArrayRW scopedArray(env, values); auto *data = counter->getCount(state).data(); - auto size = env->GetArrayLength(values); - auto *outData = scopedArray.get(); if (data == nullptr) { - memset(outData, 0, size * sizeof(uint64_t)); - } else { - memcpy(outData, data, size * sizeof(uint64_t)); + return false; } + auto size = env->GetArrayLength(values); + bool allZeros = true; + for (int i = 0; i < size; i++) { + if (data[i]) { + allZeros = false; + break; + } + } + if (allZeros) { + return false; + } + ScopedLongArrayRW scopedArray(env, values); + memcpy(scopedArray.get(), data, size * sizeof(uint64_t)); + return true; } static jobject native_toString(JNIEnv *env, jclass, jlong nativePtr) { @@ -249,7 +264,7 @@ static const JNINativeMethod g_LongArrayMultiStateCounter_methods[] = { // @CriticalNative {"native_reset", "(J)V", (void *)native_reset}, // @FastNative - {"native_getCounts", "(J[JI)V", (void *)native_getCounts}, + {"native_getCounts", "(J[JI)Z", (void *)native_getCounts}, // @FastNative {"native_toString", "(J)Ljava/lang/String;", (void *)native_toString}, // @FastNative diff --git a/core/jni/jni_wrappers.h b/core/jni/jni_wrappers.h index e3e17eed54d5..1f44994b54f2 100644 --- a/core/jni/jni_wrappers.h +++ b/core/jni/jni_wrappers.h @@ -22,6 +22,8 @@ #include <log/log.h> #include <nativehelper/JNIHelp.h> +#include <string> + namespace android { static inline jclass FindClassOrDie(JNIEnv* env, const char* class_name) { diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp index 1a0328338980..746740b0248b 100644 --- a/core/jni/platform/host/HostRuntime.cpp +++ b/core/jni/platform/host/HostRuntime.cpp @@ -48,6 +48,8 @@ using namespace std; * (see AndroidRuntime.cpp). */ +extern int register_android_media_ImageReader(JNIEnv* env); +extern int register_android_media_PublicFormatUtils(JNIEnv* env); extern int register_android_os_Binder(JNIEnv* env); extern int register_libcore_util_NativeAllocationRegistry(JNIEnv* env); @@ -126,6 +128,10 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.database.sqlite.SQLiteDebug", REG_JNI(register_android_database_SQLiteDebug)}, {"android.database.sqlite.SQLiteRawStatement", REG_JNI(register_android_database_SQLiteRawStatement)}, +#endif + {"android.media.ImageReader", REG_JNI(register_android_media_ImageReader)}, + {"android.media.PublicFormatUtils", REG_JNI(register_android_media_PublicFormatUtils)}, +#ifdef __linux__ {"android.os.Binder", REG_JNI(register_android_os_Binder)}, {"android.os.FileObserver", REG_JNI(register_android_os_FileObserver)}, {"android.os.MessageQueue", REG_JNI(register_android_os_MessageQueue)}, diff --git a/core/proto/OWNERS b/core/proto/OWNERS index b51f72dee260..aa8f8419ac46 100644 --- a/core/proto/OWNERS +++ b/core/proto/OWNERS @@ -5,7 +5,6 @@ joeo@google.com singhtejinder@google.com yanmin@google.com yaochen@google.com -yro@google.com zhouwenjie@google.com # Frameworks diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 78526ad4a06b..ee6899cf866b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7759,7 +7759,17 @@ @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @hide --> <permission android:name="android.permission.READ_BLOCKED_NUMBERS" - android:protectionLevel="signature" /> + android:protectionLevel="signature" + android:featureFlag="!android.permission.flags.grant_read_blocked_numbers_to_system_ui_intelligence" /> + + <!-- Allows the holder to read blocked numbers. See + {@link android.provider.BlockedNumberContract}. + @SystemApi + @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") + @hide --> + <permission android:name="android.permission.READ_BLOCKED_NUMBERS" + android:protectionLevel="signature|role" + android:featureFlag="android.permission.flags.grant_read_blocked_numbers_to_system_ui_intelligence" /> <!-- Allows the holder to write blocked numbers. See {@link android.provider.BlockedNumberContract}. diff --git a/core/res/res/drawable-w192dp/loader_horizontal_watch.xml b/core/res/res/drawable-w192dp/loader_horizontal_watch.xml deleted file mode 100644 index 18cea6e0d87d..000000000000 --- a/core/res/res/drawable-w192dp/loader_horizontal_watch.xml +++ /dev/null @@ -1,97 +0,0 @@ -<!-- - ~ Copyright (C) 2025 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. - --> - -<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> - <aapt:attr name="android:drawable"> - <vector android:height="15dp" android:width="67dp" android:viewportHeight="15" android:viewportWidth="67"> - <group android:name="_R_G"> - <group android:name="_R_G_L_1_G" android:translateX="33.5" android:translateY="7.5"> - <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M33.5 -7.5 C33.5,-7.5 33.5,7.5 33.5,7.5 C33.5,7.5 -33.5,7.5 -33.5,7.5 C-33.5,7.5 -33.5,-7.5 -33.5,-7.5 C-33.5,-7.5 33.5,-7.5 33.5,-7.5c "/> - </group> - <group android:name="_R_G_L_0_G" android:translateX="-296.5" android:translateY="-62.5" android:pivotX="330" android:pivotY="70" android:scaleX="0.1" android:scaleY="0.1"> - <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-224.84700000000004" android:translateY="-321.948" android:pivotX="555.09" android:pivotY="-329" android:rotation="28.9" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M194.88 359 C190,359 185.05,357.81 180.48,355.3 C59.86,289.14 -41.55,191.9 -112.79,74.11 C-186.14,-47.16 -224.91,-186.55 -224.91,-329 C-224.91,-345.57 -211.48,-359 -194.91,-359 C-178.34,-359 -164.91,-345.57 -164.91,-329 C-164.91,-197.5 -129.13,-68.84 -61.45,43.06 C4.33,151.82 97.97,241.6 209.33,302.69 C223.86,310.66 229.18,328.9 221.21,343.42 C215.75,353.37 205.48,359 194.88,359c "/> - </group> - <group android:name="_R_G_L_0_G_L_5_G" android:translateX="744.323" android:translateY="-277.96299999999997" android:pivotX="-414.08" android:pivotY="-372.985" android:rotation="28.9"> - <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-335.95 402.99 C-351.13,402.99 -364.16,391.5 -365.76,376.07 C-367.46,359.59 -355.49,344.85 -339.01,343.14 C-162.93,324.91 -0.15,242.33 119.34,110.62 C239.66,-22.01 305.92,-193.76 305.92,-372.98 C305.92,-389.55 319.35,-402.98 335.92,-402.98 C352.49,-402.98 365.92,-389.55 365.92,-372.98 C365.92,-178.82 294.13,7.24 163.78,150.93 C34.34,293.61 -142.03,383.07 -332.83,402.82 C-333.88,402.93 -334.92,402.99 -335.95,402.99c "/> - </group> - <group android:name="_R_G_L_0_G_L_2_G" android:translateX="185.385" android:translateY="70.09100000000001" android:pivotX="144.858" android:pivotY="-721.039" android:rotation="28.9" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M144.62 58.96 C144.61,58.96 144.61,58.96 144.6,58.96 C40.39,58.93 -60.82,38.66 -156.19,-1.28 C-171.48,-7.68 -178.68,-25.26 -172.28,-40.54 C-165.88,-55.82 -148.3,-63.02 -133.02,-56.62 C-45.02,-19.77 48.4,-1.07 144.63,-1.04 C161.19,-1.03 174.62,12.4 174.62,28.97 C174.61,45.53 161.18,58.96 144.62,58.96c "/> - </group> - <group android:name="_R_G_L_0_G_L_0_G" android:translateX="330" android:translateY="70"> - <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-660 -313 C-660,-313 -660,313 -660,313 C-660,313 660,313 660,313 C660,313 660,-313 660,-313 C660,-313 -660,-313 -660,-313c M300.74 -1.16 C205.46,38.62 103.22,59.09 -0.03,59.05 C-103.28,59.01 -205.51,38.48 -300.76,-1.37 C-316.05,-7.76 -323.26,-25.34 -316.86,-40.62 C-310.47,-55.91 -292.9,-63.12 -277.61,-56.72 C-189.68,-19.94 -95.32,-0.98 -0.01,-0.95 C95.3,-0.92 189.67,-19.81 277.63,-56.53 C292.92,-62.91 310.49,-55.69 316.87,-40.4 C323.25,-25.11 316.03,-7.54 300.74,-1.16c "/> - </group> - </group> - </group> - <group android:name="time_group"/> - </vector> - </aapt:attr> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="time_group"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> -</animated-vector> - diff --git a/core/res/res/drawable-w204dp/loader_horizontal_watch.xml b/core/res/res/drawable-w204dp/loader_horizontal_watch.xml deleted file mode 100644 index fbc6eab320eb..000000000000 --- a/core/res/res/drawable-w204dp/loader_horizontal_watch.xml +++ /dev/null @@ -1,104 +0,0 @@ -<!-- - ~ Copyright (C) 2025 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. - --> - -<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> - <aapt:attr name="android:drawable"> - <vector android:height="15dp" android:width="70dp" android:viewportHeight="15" android:viewportWidth="70"> - <group android:name="_R_G"> - <group android:name="_R_G_L_1_G" android:translateX="35" android:translateY="7.5"> - <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M35 -7.5 C35,-7.5 35,7.5 35,7.5 C35,7.5 -35,7.5 -35,7.5 C-35,7.5 -35,-7.5 -35,-7.5 C-35,-7.5 35,-7.5 35,-7.5c "/> - </group> - <group android:name="_R_G_L_0_G" android:translateX="-310" android:translateY="-64" android:pivotX="345" android:pivotY="71.5" android:scaleX="0.1" android:scaleY="0.1"> - <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-239.44799999999998" android:translateY="-341.45" android:pivotX="584.448" android:pivotY="-346.55" android:rotation="28.8" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M205.28 376.55 C200.4,376.55 195.46,375.36 190.88,372.85 C64.08,303.29 -42.54,201.07 -117.44,77.24 C-194.55,-50.25 -235.31,-196.79 -235.31,-346.55 C-235.31,-363.12 -221.88,-376.55 -205.31,-376.55 C-188.74,-376.55 -175.31,-363.12 -175.31,-346.55 C-175.31,-207.74 -137.54,-71.93 -66.1,46.19 C3.34,160.99 102.18,255.76 219.73,320.24 C234.26,328.21 239.58,346.45 231.61,360.97 C226.15,370.92 215.88,376.55 205.28,376.55c "/> - </group> - <group android:name="_R_G_L_0_G_L_5_G" android:translateX="781.413" android:translateY="-295.124" android:pivotX="-436.413" android:pivotY="-392.876" android:rotation="28.8"> - <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-353.86 422.88 C-369.04,422.88 -382.07,411.4 -383.67,395.97 C-385.37,379.49 -373.4,364.74 -356.92,363.03 C-171.06,343.79 0.76,256.62 126.89,117.59 C253.89,-22.41 323.83,-203.7 323.83,-392.88 C323.83,-409.44 337.26,-422.88 353.83,-422.88 C370.4,-422.88 383.83,-409.44 383.83,-392.88 C383.83,-188.76 308.36,6.84 171.32,157.9 C35.25,307.89 -150.15,401.94 -350.74,422.72 C-351.79,422.82 -352.83,422.88 -353.86,422.88c "/> - </group> - <group android:name="_R_G_L_0_G_L_2_G" android:translateX="192.671" android:translateY="71.49599999999998" android:pivotX="152.329" android:pivotY="-759.496" android:rotation="28.8" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M152.33 60.5 C152.33,60.5 152.32,60.5 152.32,60.5 C42.76,60.47 -63.64,39.16 -163.91,-2.82 C-179.19,-9.22 -186.39,-26.8 -179.99,-42.08 C-173.59,-57.36 -156.02,-64.57 -140.73,-58.16 C-47.84,-19.27 50.77,0.47 152.34,0.5 C168.91,0.51 182.33,13.94 182.33,30.51 C182.32,47.08 168.89,60.5 152.33,60.5c "/> - </group> - <group android:name="_R_G_L_0_G_L_0_G" android:translateX="345" android:translateY="71.5"> - <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-579 -259.5 C-579,-259.5 -579,259.5 -579,259.5 C-579,259.5 579,259.5 579,259.5 C579,259.5 579,-259.5 579,-259.5 C579,-259.5 -579,-259.5 -579,-259.5c M316.17 -2.8 C216,39.02 108.52,60.54 -0.03,60.5 C-108.58,60.46 -216.04,38.87 -316.18,-3.02 C-331.47,-9.41 -338.68,-26.99 -332.28,-42.27 C-325.89,-57.56 -308.32,-64.76 -293.03,-58.37 C-200.22,-19.55 -100.61,0.46 -0.01,0.5 C100.6,0.54 200.22,-19.41 293.06,-58.17 C308.35,-64.55 325.92,-57.33 332.3,-42.04 C338.68,-26.75 331.46,-9.18 316.17,-2.8c "/> - </group> - </group> - </group> - <group android:name="time_group"/> - </vector> - </aapt:attr> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="time_group"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> -</animated-vector> - diff --git a/core/res/res/drawable-w216dp/loader_horizontal_watch.xml b/core/res/res/drawable-w216dp/loader_horizontal_watch.xml deleted file mode 100644 index ed4b7ea0ff02..000000000000 --- a/core/res/res/drawable-w216dp/loader_horizontal_watch.xml +++ /dev/null @@ -1,105 +0,0 @@ -<!-- - ~ Copyright (C) 2025 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. - --> - -<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> - <aapt:attr name="android:drawable"> - <vector android:height="16dp" android:width="74dp" android:viewportHeight="16" android:viewportWidth="74"> - <group android:name="_R_G"> - <group android:name="_R_G_L_1_G" android:translateX="37" android:translateY="8"> - <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M37 -8 C37,-8 37,8 37,8 C37,8 -37,8 -37,8 C-37,8 -37,-8 -37,-8 C-37,-8 37,-8 37,-8c "/> - </group> - <group android:name="_R_G_L_0_G" android:translateX="-328" android:translateY="-65.5" android:pivotX="365" android:pivotY="73.5" android:scaleX="0.1" android:scaleY="0.1"> - <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-256.447" android:translateY="-365.014" android:pivotX="621.447" android:pivotY="-368.486" android:rotation="28.8" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M218.28 398.49 C213.4,398.49 208.46,397.3 203.88,394.78 C69.34,320.99 -43.78,212.53 -123.25,81.15 C-205.06,-54.11 -248.31,-209.59 -248.31,-368.49 C-248.31,-385.05 -234.88,-398.49 -218.31,-398.49 C-201.74,-398.49 -188.31,-385.05 -188.31,-368.49 C-188.31,-220.54 -148.06,-75.8 -71.91,50.09 C2.1,172.45 107.45,273.45 232.73,342.18 C247.26,350.15 252.58,368.38 244.61,382.91 C239.15,392.86 228.88,398.49 218.28,398.49c "/> - </group> - <group android:name="_R_G_L_0_G_L_5_G" android:translateX="829.0260000000001" android:translateY="-315.759" android:pivotX="-464.026" android:pivotY="-417.741" android:rotation="28.8"> - <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-376.25 447.74 C-391.43,447.74 -404.46,436.26 -406.05,420.83 C-407.76,404.35 -395.78,389.61 -379.3,387.9 C-181.22,367.38 1.9,274.48 136.32,126.3 C271.67,-22.9 346.22,-216.12 346.22,-417.74 C346.22,-434.31 359.65,-447.74 376.22,-447.74 C392.79,-447.74 406.22,-434.31 406.22,-417.74 C406.22,-201.18 326.15,6.35 180.76,166.61 C36.39,325.75 -160.31,425.54 -373.12,447.58 C-374.17,447.69 -375.22,447.74 -376.25,447.74c "/> - </group> - <group android:name="_R_G_L_0_G_L_2_G" android:translateX="203.029" android:translateY="74.06899999999996" android:pivotX="161.971" android:pivotY="-807.569" android:rotation="28.8" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M161.97 62.43 C161.97,62.43 161.96,62.43 161.96,62.43 C45.71,62.4 -67.17,39.79 -173.55,-4.75 C-188.83,-11.15 -196.03,-28.72 -189.63,-44.01 C-183.24,-59.29 -165.66,-66.49 -150.38,-60.09 C-51.37,-18.64 53.72,2.4 161.98,2.43 C178.55,2.44 191.98,15.87 191.97,32.44 C191.97,49 178.54,62.43 161.97,62.43c "/> - </group> - <group android:name="_R_G_L_0_G_L_0_G" android:translateX="365" android:translateY="73.5"> - <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-609 -244.5 C-609,-244.5 -609,244.5 -609,244.5 C-609,244.5 609,244.5 609,244.5 C609,244.5 609,-244.5 609,-244.5 C609,-244.5 -609,-244.5 -609,-244.5c M335.44 -4.16 C229.17,40.21 115.13,63.04 -0.04,63 C-115.21,62.96 -229.22,40.05 -335.47,-4.39 C-350.76,-10.79 -357.95,-28.36 -351.56,-43.65 C-345.17,-58.93 -327.59,-66.14 -312.31,-59.74 C-213.39,-18.36 -107.24,2.96 -0.02,3 C107.21,3.04 213.38,-18.22 312.33,-59.53 C327.62,-65.91 345.19,-58.69 351.57,-43.4 C357.95,-28.11 350.73,-10.54 335.44,-4.16c "/> - </group> - </group> - </group> - <group android:name="time_group"/> - </vector> - </aapt:attr> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="time_group"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> -</animated-vector> - - diff --git a/core/res/res/drawable-w228dp/loader_horizontal_watch.xml b/core/res/res/drawable-w228dp/loader_horizontal_watch.xml deleted file mode 100644 index 6b86c634d554..000000000000 --- a/core/res/res/drawable-w228dp/loader_horizontal_watch.xml +++ /dev/null @@ -1,103 +0,0 @@ -<!-- - ~ Copyright (C) 2025 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. - --> - -<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> - <aapt:attr name="android:drawable"> - <vector android:height="14dp" android:width="76dp" android:viewportHeight="14" android:viewportWidth="76"> - <group android:name="_R_G"> - <group android:name="_R_G_L_1_G" android:translateX="39" android:translateY="8"> - <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M39 -8 C39,-8 39,8 39,8 C39,8 -39,8 -39,8 C-39,8 -39,-8 -39,-8 C-39,-8 39,-8 39,-8c "/> - </group> - <group android:name="_R_G_L_0_G" android:translateX="-345" android:translateY="-67" android:pivotX="384" android:pivotY="75" android:scaleX="0.1" android:scaleY="0.1"> - <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-274.19" android:translateY="-390.077" android:pivotX="658.448" android:pivotY="-390.423" android:rotation="28.7" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M231.28 420.42 C226.4,420.42 221.45,419.23 216.88,416.72 C74.61,338.68 -45.02,224 -129.06,85.06 C-171.54,14.82 -204.38,-60.73 -226.66,-139.5 C-249.65,-220.76 -261.31,-305.18 -261.31,-390.42 C-261.31,-406.99 -247.88,-420.42 -231.31,-420.42 C-214.74,-420.42 -201.31,-406.99 -201.31,-390.42 C-201.31,-310.71 -190.42,-231.78 -168.93,-155.83 C-148.11,-82.23 -117.42,-11.63 -77.72,54 C0.86,183.92 112.71,291.15 245.73,364.12 C260.26,372.08 265.58,390.32 257.61,404.85 C252.15,414.79 241.88,420.42 231.28,420.42c "/> - </group> - <group android:name="_R_G_L_0_G_L_5_G" android:translateX="875.8979999999999" android:translateY="-337.894" android:pivotX="-491.64" android:pivotY="-442.606" android:rotation="28.7"> - <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-398.64 472.61 C-413.82,472.61 -426.84,461.13 -428.44,445.7 C-430.15,429.22 -418.17,414.47 -401.69,412.77 C-191.38,390.98 3.04,292.33 145.75,135.01 C289.46,-23.4 368.6,-228.54 368.6,-442.61 C368.6,-459.17 382.04,-472.61 398.6,-472.61 C415.17,-472.61 428.6,-459.17 428.6,-442.61 C428.6,-213.6 343.93,5.85 190.19,175.33 C37.53,343.61 -170.48,449.13 -395.51,472.44 C-396.56,472.55 -397.6,472.61 -398.64,472.61c "/> - </group> - <group android:name="_R_G_L_0_G_L_2_G" android:translateX="212.64499999999998" android:translateY="75.14200000000005" android:pivotX="171.613" android:pivotY="-855.642" android:rotation="28.7" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M171.61 64.36 C171.61,64.36 171.61,64.36 171.61,64.36 C48.68,64.32 -70.7,40.42 -183.19,-6.68 C-198.47,-13.07 -205.68,-30.65 -199.28,-45.93 C-192.88,-61.22 -175.3,-68.42 -160.02,-62.02 C-54.9,-18.01 56.68,4.33 171.62,4.36 C188.19,4.36 201.62,17.8 201.61,34.36 C201.61,50.93 188.18,64.36 171.61,64.36c "/> - </group> - <group android:name="_R_G_L_0_G_L_0_G" android:translateX="384" android:translateY="75"> - <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-611 -259 C-611,-259 -611,259 -611,259 C-611,259 611,259 611,259 C611,259 611,-259 611,-259 C611,-259 -611,-259 -611,-259c M354.66 -6.52 C242.36,40.4 121.76,64.54 -0.04,64.5 C-121.84,64.46 -242.44,40.23 -354.74,-6.76 C-370.04,-13.16 -377.24,-30.73 -370.84,-46.02 C-364.44,-61.3 -346.94,-68.51 -331.64,-62.12 C-226.54,-18.18 -113.84,4.46 -0.04,4.5 C113.76,4.54 226.56,-18.02 331.56,-61.89 C346.86,-68.27 364.46,-61.05 370.86,-45.76 C377.26,-30.47 369.96,-12.9 354.66,-6.52c "/> - </group> - </group> - </group> - <group android:name="time_group"/> - </vector> - </aapt:attr> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="time_group"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> -</animated-vector> diff --git a/core/res/res/drawable-w240dp/loader_horizontal_watch.xml b/core/res/res/drawable-w240dp/loader_horizontal_watch.xml deleted file mode 100644 index ad60bbdc420c..000000000000 --- a/core/res/res/drawable-w240dp/loader_horizontal_watch.xml +++ /dev/null @@ -1,104 +0,0 @@ -<!-- - ~ Copyright (C) 2025 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. - --> - -<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> - <aapt:attr name="android:drawable"> - <vector android:height="17dp" android:width="82dp" android:viewportHeight="17" android:viewportWidth="82"> - <group android:name="_R_G"> - <group android:name="_R_G_L_1_G" android:translateX="41" android:translateY="8.5"> - <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M41 -8.5 C41,-8.5 41,8.5 41,8.5 C41,8.5 -41,8.5 -41,8.5 C-41,8.5 -41,-8.5 -41,-8.5 C-41,-8.5 41,-8.5 41,-8.5c "/> - </group> - <group android:name="_R_G_L_0_G" android:translateX="-362.5" android:translateY="-69" android:pivotX="403.5" android:pivotY="77.5" android:scaleX="0.1" android:scaleY="0.1"> - <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-291.64799999999997" android:translateY="-414.141" android:pivotX="695.448" android:pivotY="-412.359" android:rotation="28.7" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M244.28 442.36 C239.4,442.36 234.45,441.17 229.88,438.66 C79.87,356.38 -46.26,235.46 -134.87,88.96 C-179.66,14.91 -214.28,-64.74 -237.78,-147.79 C-262.02,-233.47 -274.31,-322.49 -274.31,-412.36 C-274.31,-428.93 -260.88,-442.36 -244.31,-442.36 C-227.74,-442.36 -214.31,-428.93 -214.31,-412.36 C-214.31,-328.01 -202.78,-244.49 -180.05,-164.12 C-158.01,-86.24 -125.54,-11.54 -83.53,57.91 C-0.38,195.38 117.97,308.85 258.73,386.05 C273.26,394.02 278.58,412.26 270.61,426.78 C265.15,436.73 254.88,442.36 244.28,442.36c "/> - </group> - <group android:name="_R_G_L_0_G_L_5_G" android:translateX="923.0530000000001" android:translateY="-359.029" android:pivotX="-519.253" android:pivotY="-467.471" android:rotation="28.7"> - <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-421.02 497.47 C-436.2,497.47 -449.23,485.99 -450.83,470.56 C-452.53,454.08 -440.56,439.34 -424.08,437.63 C-201.54,414.57 4.18,310.19 155.19,143.73 C229.54,61.77 287.7,-31.75 328.04,-134.22 C369.81,-240.3 390.99,-352.42 390.99,-467.47 C390.99,-484.04 404.42,-497.47 420.99,-497.47 C437.56,-497.47 450.99,-484.04 450.99,-467.47 C450.99,-226.02 361.72,5.35 199.63,184.04 C38.67,361.47 -180.63,472.73 -417.89,497.31 C-418.94,497.42 -419.99,497.47 -421.02,497.47c "/> - </group> - <group android:name="_R_G_L_0_G_L_2_G" android:translateX="222.54600000000002" android:translateY="77.21400000000006" android:pivotX="181.254" android:pivotY="-903.714" android:rotation="28.7" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M181.26 66.28 C181.25,66.28 181.25,66.28 181.25,66.28 C51.64,66.25 -74.22,41.06 -192.83,-8.6 C-208.12,-15 -215.32,-32.58 -208.92,-47.86 C-202.52,-63.15 -184.94,-70.35 -169.66,-63.95 C-58.42,-17.38 59.64,6.25 181.26,6.28 C197.83,6.29 211.26,19.72 211.26,36.29 C211.25,52.86 197.82,66.28 181.26,66.28c "/> - </group> - <group android:name="_R_G_L_0_G_L_0_G" android:translateX="403.5" android:translateY="77.5"> - <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-630.5 -255.5 C-630.5,-255.5 -630.5,255.5 -630.5,255.5 C-630.5,255.5 630.5,255.5 630.5,255.5 C630.5,255.5 630.5,-255.5 630.5,-255.5 C630.5,-255.5 -630.5,-255.5 -630.5,-255.5c M374 -8.88 C255.5,40.59 128.4,66.04 0,66 C-128.4,65.95 -255.6,40.42 -374,-9.14 C-389.3,-15.53 -396.5,-33.11 -390.1,-48.39 C-383.7,-63.68 -366.2,-70.88 -350.9,-64.49 C-239.7,-18 -120.5,5.96 0,6 C120.4,6.04 239.7,-17.84 350.9,-64.25 C366.2,-70.63 383.7,-63.41 390.1,-48.12 C396.5,-32.83 389.3,-15.26 374,-8.88c "/> - </group> - </group> - </group> - <group android:name="time_group"/> - </vector> - </aapt:attr> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="time_group"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> -</animated-vector> - diff --git a/core/res/res/drawable/loader_horizontal_watch.xml b/core/res/res/drawable/loader_horizontal_watch.xml deleted file mode 100644 index 6b86c634d554..000000000000 --- a/core/res/res/drawable/loader_horizontal_watch.xml +++ /dev/null @@ -1,103 +0,0 @@ -<!-- - ~ Copyright (C) 2025 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. - --> - -<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> - <aapt:attr name="android:drawable"> - <vector android:height="14dp" android:width="76dp" android:viewportHeight="14" android:viewportWidth="76"> - <group android:name="_R_G"> - <group android:name="_R_G_L_1_G" android:translateX="39" android:translateY="8"> - <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M39 -8 C39,-8 39,8 39,8 C39,8 -39,8 -39,8 C-39,8 -39,-8 -39,-8 C-39,-8 39,-8 39,-8c "/> - </group> - <group android:name="_R_G_L_0_G" android:translateX="-345" android:translateY="-67" android:pivotX="384" android:pivotY="75" android:scaleX="0.1" android:scaleY="0.1"> - <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-274.19" android:translateY="-390.077" android:pivotX="658.448" android:pivotY="-390.423" android:rotation="28.7" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M231.28 420.42 C226.4,420.42 221.45,419.23 216.88,416.72 C74.61,338.68 -45.02,224 -129.06,85.06 C-171.54,14.82 -204.38,-60.73 -226.66,-139.5 C-249.65,-220.76 -261.31,-305.18 -261.31,-390.42 C-261.31,-406.99 -247.88,-420.42 -231.31,-420.42 C-214.74,-420.42 -201.31,-406.99 -201.31,-390.42 C-201.31,-310.71 -190.42,-231.78 -168.93,-155.83 C-148.11,-82.23 -117.42,-11.63 -77.72,54 C0.86,183.92 112.71,291.15 245.73,364.12 C260.26,372.08 265.58,390.32 257.61,404.85 C252.15,414.79 241.88,420.42 231.28,420.42c "/> - </group> - <group android:name="_R_G_L_0_G_L_5_G" android:translateX="875.8979999999999" android:translateY="-337.894" android:pivotX="-491.64" android:pivotY="-442.606" android:rotation="28.7"> - <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-398.64 472.61 C-413.82,472.61 -426.84,461.13 -428.44,445.7 C-430.15,429.22 -418.17,414.47 -401.69,412.77 C-191.38,390.98 3.04,292.33 145.75,135.01 C289.46,-23.4 368.6,-228.54 368.6,-442.61 C368.6,-459.17 382.04,-472.61 398.6,-472.61 C415.17,-472.61 428.6,-459.17 428.6,-442.61 C428.6,-213.6 343.93,5.85 190.19,175.33 C37.53,343.61 -170.48,449.13 -395.51,472.44 C-396.56,472.55 -397.6,472.61 -398.64,472.61c "/> - </group> - <group android:name="_R_G_L_0_G_L_2_G" android:translateX="212.64499999999998" android:translateY="75.14200000000005" android:pivotX="171.613" android:pivotY="-855.642" android:rotation="28.7" android:scaleY="0"> - <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M171.61 64.36 C171.61,64.36 171.61,64.36 171.61,64.36 C48.68,64.32 -70.7,40.42 -183.19,-6.68 C-198.47,-13.07 -205.68,-30.65 -199.28,-45.93 C-192.88,-61.22 -175.3,-68.42 -160.02,-62.02 C-54.9,-18.01 56.68,4.33 171.62,4.36 C188.19,4.36 201.62,17.8 201.61,34.36 C201.61,50.93 188.18,64.36 171.61,64.36c "/> - </group> - <group android:name="_R_G_L_0_G_L_0_G" android:translateX="384" android:translateY="75"> - <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-611 -259 C-611,-259 -611,259 -611,259 C-611,259 611,259 611,259 C611,259 611,-259 611,-259 C611,-259 -611,-259 -611,-259c M354.66 -6.52 C242.36,40.4 121.76,64.54 -0.04,64.5 C-121.84,64.46 -242.44,40.23 -354.74,-6.76 C-370.04,-13.16 -377.24,-30.73 -370.84,-46.02 C-364.44,-61.3 -346.94,-68.51 -331.64,-62.12 C-226.54,-18.18 -113.84,4.46 -0.04,4.5 C113.76,4.54 226.56,-18.02 331.56,-61.89 C346.86,-68.27 364.46,-61.05 370.86,-45.76 C377.26,-30.47 369.96,-12.9 354.66,-6.52c "/> - </group> - </group> - </group> - <group android:name="time_group"/> - </vector> - </aapt:attr> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_6_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_5_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_L_2_G"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> - <target android:name="time_group"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> - </set> - </aapt:attr> - </target> -</animated-vector> diff --git a/core/res/res/drawable/progress_ring_watch.xml b/core/res/res/drawable/progress_ring_watch.xml index 8250ee600a8f..2e65ff13b3de 100644 --- a/core/res/res/drawable/progress_ring_watch.xml +++ b/core/res/res/drawable/progress_ring_watch.xml @@ -16,27 +16,27 @@ --> <rotate xmlns:android="http://schemas.android.com/apk/res/android" - android:fromDegrees = "270" - android:toDegrees="270" - android:pivotX="50%" - android:pivotY="50%" > + android:fromDegrees="270" + android:toDegrees="270"> <layer-list> <item> <shape - android:shape="ring" - android:innerRadiusRatio="@dimen/progressbar_inner_radius_ratio" + android:shape="arc" + android:useLevel="false" android:thickness="@dimen/progressbar_thickness" - android:useLevel="false"> - <solid android:color="@color/materialColorSurfaceContainer"/> + android:innerRadiusRatio="@dimen/progressbar_inner_radius_ratio" + android:strokeCap="round"> + <solid android:color="@*android:color/materialColorSurfaceContainer"/> </shape> </item> <item> <shape - android:shape="ring" - android:innerRadiusRatio="@dimen/progressbar_inner_radius_ratio" + android:shape="arc" + android:useLevel="true" android:thickness="@dimen/progressbar_thickness" - android:useLevel="true"> - <solid android:color="@color/materialColorPrimary"/> + android:innerRadiusRatio="@dimen/progressbar_inner_radius_ratio" + android:strokeCap="round"> + <solid android:color="@*android:color/materialColorPrimary"/> </shape> </item> </layer-list> diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml index 1bde17358825..68096f8cc50e 100644 --- a/core/res/res/layout/notification_2025_conversation_header.xml +++ b/core/res/res/layout/notification_2025_conversation_header.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2024 The Android Open Source Project + ~ Copyright (C) 2025 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. @@ -15,157 +15,74 @@ ~ limitations under the License --> -<com.android.internal.widget.ConversationHeaderLinearLayout +<!-- extends RelativeLayout --> +<NotificationHeaderView xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/conversation_header" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:id="@+id/notification_header" + android:layout_width="match_parent" + android:layout_height="@dimen/notification_2025_header_height" + android:clipChildren="false" + android:gravity="center_vertical" android:orientation="horizontal" - android:paddingTop="@dimen/notification_2025_margin" + android:theme="@style/Theme.DeviceDefault.Notification" + android:importantForAccessibility="no" > - <TextView - android:id="@+id/conversation_text" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="16sp" - android:singleLine="true" - android:layout_weight="1" - /> - - <TextView - android:id="@+id/app_name_divider" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:text="@string/notification_header_divider_symbol" - android:singleLine="true" + <ImageView + android:id="@+id/left_icon" + android:layout_width="@dimen/notification_2025_left_icon_size" + android:layout_height="@dimen/notification_2025_left_icon_size" + android:layout_alignParentStart="true" + android:layout_margin="@dimen/notification_2025_margin" + android:background="@drawable/notification_large_icon_outline" + android:clipToOutline="true" + android:importantForAccessibility="no" + android:scaleType="centerCrop" android:visibility="gone" /> - <!-- App Name --> - <com.android.internal.widget.ObservableTextView - android:id="@+id/app_name_text" - android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:singleLine="true" - android:visibility="gone" - /> + <include layout="@layout/notification_2025_conversation_icon_container" /> - <TextView - android:id="@+id/time_divider" + <!-- extends ViewGroup --> + <NotificationTopLineView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/notification_top_line" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:text="@string/notification_header_divider_symbol" - android:singleLine="true" - android:visibility="gone" - /> + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/expand_button" + android:layout_alignWithParentIfMissing="true" + android:layout_marginVertical="@dimen/notification_2025_margin" + android:clipChildren="false" + android:gravity="center_vertical" + android:paddingStart="@dimen/notification_2025_content_margin_start" + android:theme="@style/Theme.DeviceDefault.Notification" + > - <DateTimeView - android:id="@+id/time" - android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Time" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:showRelative="true" - android:singleLine="true" - android:visibility="gone" - /> + <include layout="@layout/notification_2025_top_line_views" /> - <ViewStub - android:id="@+id/chronometer" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:layout="@layout/notification_template_part_chronometer" - android:visibility="gone" - /> + </NotificationTopLineView> - <TextView - android:id="@+id/verification_divider" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:text="@string/notification_header_divider_symbol" - android:singleLine="true" - android:visibility="gone" - /> - - <ImageView - android:id="@+id/verification_icon" - android:layout_width="@dimen/notification_verification_icon_size" - android:layout_height="@dimen/notification_verification_icon_size" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:baseline="10dp" - android:scaleType="fitCenter" - android:src="@drawable/ic_notifications_alerted" - android:visibility="gone" + <FrameLayout + android:id="@+id/alternate_expand_target" + android:layout_width="@dimen/notification_2025_content_margin_start" + android:layout_height="match_parent" + android:layout_alignParentStart="true" + android:importantForAccessibility="no" + android:focusable="false" /> - <TextView - android:id="@+id/verification_text" - android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + <include layout="@layout/notification_2025_expand_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:layout_weight="100" - android:showRelative="true" - android:singleLine="true" - android:visibility="gone" - /> + android:layout_gravity="top|end" + android:layout_alignParentEnd="true" /> - <ImageButton - android:id="@+id/feedback" - android:layout_width="@dimen/notification_feedback_size" - android:layout_height="@dimen/notification_feedback_size" - android:layout_marginStart="@dimen/notification_header_separating_margin" - android:background="?android:selectableItemBackgroundBorderless" - android:contentDescription="@string/notification_feedback_indicator" - android:baseline="13dp" - android:scaleType="fitCenter" - android:src="@drawable/ic_feedback_indicator" - android:visibility="gone" - /> + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_alignParentTop="true" + android:layout_alignParentEnd="true" /> - <ImageView - android:id="@+id/phishing_alert" - android:layout_width="@dimen/notification_2025_badge_size" - android:layout_height="@dimen/notification_2025_badge_size" - android:layout_marginStart="@dimen/notification_2025_badge_margin" - android:baseline="@dimen/notification_2025_badge_baseline" - android:scaleType="fitCenter" - android:src="@drawable/ic_dialog_alert_material" - android:visibility="gone" - android:contentDescription="@string/notification_phishing_alert_content_description" - /> - - <ImageView - android:id="@+id/profile_badge" - android:layout_width="@dimen/notification_2025_badge_size" - android:layout_height="@dimen/notification_2025_badge_size" - android:layout_marginStart="@dimen/notification_2025_badge_margin" - android:baseline="@dimen/notification_2025_badge_baseline" - android:scaleType="fitCenter" - android:visibility="gone" - android:contentDescription="@string/notification_work_profile_content_description" - /> - - <ImageView - android:id="@+id/alerted_icon" - android:layout_width="@dimen/notification_2025_badge_size" - android:layout_height="@dimen/notification_2025_badge_size" - android:layout_marginStart="@dimen/notification_2025_badge_margin" - android:baseline="@dimen/notification_2025_badge_baseline" - android:contentDescription="@string/notification_alerted_content_description" - android:scaleType="fitCenter" - android:src="@drawable/ic_notifications_alerted" - android:visibility="gone" - /> -</com.android.internal.widget.ConversationHeaderLinearLayout> +</NotificationHeaderView> diff --git a/core/res/res/layout/notification_2025_template_collapsed_call.xml b/core/res/res/layout/notification_2025_template_collapsed_call.xml index 6f3c15adb082..ee691e4d6894 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_call.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_call.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?><!-- - ~ Copyright (C) 2024 The Android Open Source Project + ~ Copyright (C) 2025 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. @@ -25,55 +25,177 @@ android:theme="@style/Theme.DeviceDefault.Notification" > - <!-- CallLayout shares visual appearance with ConversationLayout, so shares layouts --> - <include layout="@layout/notification_2025_conversation_icon_container" /> - <LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" - android:minHeight="@dimen/notification_2025_min_height" - android:orientation="horizontal" + android:clipChildren="false" + android:orientation="vertical" > - <LinearLayout - android:id="@+id/notification_main_column" + <com.android.internal.widget.NotificationMaxHeightFrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_weight="1" - android:layout_marginStart="@dimen/notification_2025_content_margin_start" - android:orientation="vertical" - android:paddingBottom="@dimen/notification_2025_margin" + android:minHeight="@dimen/notification_2025_min_height" + android:clipChildren="false" > - <include - layout="@layout/notification_2025_conversation_header" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + <ImageView + android:id="@+id/left_icon" + android:layout_width="@dimen/notification_2025_left_icon_size" + android:layout_height="@dimen/notification_2025_left_icon_size" + android:layout_alignParentStart="true" + android:layout_margin="@dimen/notification_2025_margin" + android:background="@drawable/notification_large_icon_outline" + android:clipToOutline="true" + android:importantForAccessibility="no" + android:scaleType="centerCrop" + android:visibility="gone" /> - <include layout="@layout/notification_template_text" - android:layout_height="wrap_content" - android:minHeight="@dimen/notification_text_height" + <!-- CallLayout shares visual appearance with ConversationLayout, so shares layouts --> + <include layout="@layout/notification_2025_conversation_icon_container" /> + + <FrameLayout + android:id="@+id/alternate_expand_target" + android:layout_width="@dimen/notification_2025_content_margin_start" + android:layout_height="match_parent" + android:layout_gravity="start" + android:importantForAccessibility="no" + android:focusable="false" /> - </LinearLayout> + <LinearLayout + android:id="@+id/notification_headerless_view_row" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginStart="@dimen/notification_2025_content_margin_start" + android:orientation="horizontal" + android:clipChildren="false" + > - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:minWidth="@dimen/notification_content_margin_end" - > + <LinearLayout + android:id="@+id/notification_headerless_view_column" + android:layout_width="0px" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:layout_marginBottom="@dimen/notification_2025_margin" + android:layout_marginTop="@dimen/notification_2025_margin" + android:clipChildren="false" + android:orientation="vertical" + > - <include - layout="@layout/notification_2025_expand_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="top|end" - /> + <NotificationTopLineView + android:id="@+id/notification_top_line" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minHeight="@dimen/notification_headerless_line_height" + android:clipChildren="false" + android:theme="@style/Theme.DeviceDefault.Notification" + > + + <!-- + NOTE: The notification_2025_top_line_views layout contains the app_name_text. + In order to include the title view at the beginning, the Notification.Builder + has logic to hide that view whenever this title view is to be visible. + --> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:ellipsize="end" + android:fadingEdge="horizontal" + android:singleLine="true" + android:textAlignment="viewStart" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" + /> + + <include layout="@layout/notification_2025_top_line_views" /> + + </NotificationTopLineView> - </FrameLayout> + <LinearLayout + android:id="@+id/notification_main_column" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:clipChildren="false" + > + <com.android.internal.widget.NotificationVanishingFrameLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="@dimen/notification_headerless_line_height" + > + <!-- This is the simplest way to keep this text vertically centered without + gravity="center_vertical" which causes jumpiness in expansion animations. --> + <include + layout="@layout/notification_2025_text" + android:layout_width="match_parent" + android:layout_height="@dimen/notification_text_height" + android:layout_gravity="center_vertical" + android:layout_marginTop="0dp" + /> + </com.android.internal.widget.NotificationVanishingFrameLayout> + </LinearLayout> + </LinearLayout> + + <ImageView + android:id="@+id/right_icon" + android:layout_width="@dimen/notification_right_icon_size" + android:layout_height="@dimen/notification_right_icon_size" + android:layout_gravity="center_vertical|end" + android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" + android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" + android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:background="@drawable/notification_large_icon_outline" + android:clipToOutline="true" + android:importantForAccessibility="no" + android:scaleType="centerCrop" + /> + + <FrameLayout + android:id="@+id/expand_button_touch_container" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:minWidth="@dimen/notification_content_margin_end" + > + + <include layout="@layout/notification_2025_expand_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top|end" + /> + + </FrameLayout> + + </LinearLayout> + + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_gravity="top|end" /> + + </com.android.internal.widget.NotificationMaxHeightFrameLayout> + + <LinearLayout + android:id="@+id/notification_action_list_margin_target" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="-20dp" + android:clipChildren="false" + android:orientation="vertical"> + <include layout="@layout/notification_template_smart_reply_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_content_margin" + android:layout_marginStart="@dimen/notification_2025_content_margin_start" + android:layout_marginEnd="@dimen/notification_content_margin_end" /> + <include layout="@layout/notification_material_action_list" /> + </LinearLayout> </LinearLayout> </com.android.internal.widget.CallLayout> diff --git a/core/res/res/layout/notification_2025_template_collapsed_conversation.xml b/core/res/res/layout/notification_2025_template_collapsed_conversation.xml new file mode 100644 index 000000000000..f80411103501 --- /dev/null +++ b/core/res/res/layout/notification_2025_template_collapsed_conversation.xml @@ -0,0 +1,216 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 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 + --> + +<!-- extends FrameLayout --> +<com.android.internal.widget.ConversationLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/status_bar_latest_event_content" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:tag="conversation" + > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:orientation="vertical" + > + + + <com.android.internal.widget.NotificationMaxHeightFrameLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="@dimen/notification_2025_min_height" + android:clipChildren="false" + > + + <ImageView + android:id="@+id/left_icon" + android:layout_width="@dimen/notification_2025_left_icon_size" + android:layout_height="@dimen/notification_2025_left_icon_size" + android:layout_alignParentStart="true" + android:layout_margin="@dimen/notification_2025_margin" + android:background="@drawable/notification_large_icon_outline" + android:clipToOutline="true" + android:importantForAccessibility="no" + android:scaleType="centerCrop" + android:visibility="gone" + /> + + <include layout="@layout/notification_2025_conversation_icon_container" /> + + <FrameLayout + android:id="@+id/alternate_expand_target" + android:layout_width="@dimen/notification_2025_content_margin_start" + android:layout_height="match_parent" + android:layout_gravity="start" + android:importantForAccessibility="no" + android:focusable="false" + /> + + <!-- + NOTE: to make the expansion animation of id/notification_messaging happen vertically, + its X positioning must be the left edge of the notification, so instead of putting the + layout_marginStart on the id/notification_headerless_view_row, we put it on + id/notification_top_line, making the layout here just a bit different from the base. + --> + <LinearLayout + android:id="@+id/notification_headerless_view_row" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal" + android:clipChildren="false" + > + + <!-- + NOTE: because messaging will always have 2 lines, this LinearLayout should NOT + have the id/notification_headerless_view_column, as that is used for modifying + vertical margins to accommodate the single-line state that base supports + --> + <LinearLayout + android:layout_width="0px" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:layout_marginBottom="@dimen/notification_2025_margin" + android:layout_marginTop="@dimen/notification_2025_margin" + android:layout_marginStart="@dimen/notification_2025_content_margin_start" + android:clipChildren="false" + android:orientation="vertical" + > + + <NotificationTopLineView + android:id="@+id/notification_top_line" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minHeight="@dimen/notification_headerless_line_height" + android:clipChildren="false" + android:theme="@style/Theme.DeviceDefault.Notification" + > + + <!-- + NOTE: The notification_2025_top_line_views layout contains the app_name_text. + In order to include the title view at the beginning, the Notification.Builder + has logic to hide that view whenever this title view is to be visible. + --> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:ellipsize="end" + android:fadingEdge="horizontal" + android:singleLine="true" + android:textAlignment="viewStart" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" + /> + + <include layout="@layout/notification_2025_top_line_views" /> + + </NotificationTopLineView> + + <LinearLayout + android:id="@+id/notification_main_column" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:clipChildren="false" + > + <com.android.internal.widget.MessagingLinearLayout + android:id="@+id/notification_messaging" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:spacing="@dimen/notification_messaging_spacing" /> + </LinearLayout> + + </LinearLayout> + + <!-- Images --> + <com.android.internal.widget.MessagingLinearLayout + android:id="@+id/conversation_image_message_container" + android:layout_width="@dimen/notification_right_icon_size" + android:layout_height="@dimen/notification_right_icon_size" + android:layout_gravity="center_vertical|end" + android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" + android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" + android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:forceHasOverlappingRendering="false" + android:spacing="0dp" + android:clipChildren="false" + android:visibility="gone" + /> + + <ImageView + android:id="@+id/right_icon" + android:layout_width="@dimen/notification_right_icon_size" + android:layout_height="@dimen/notification_right_icon_size" + android:layout_gravity="center_vertical|end" + android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" + android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" + android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:background="@drawable/notification_large_icon_outline" + android:clipToOutline="true" + android:importantForAccessibility="no" + android:scaleType="centerCrop" + /> + + <FrameLayout + android:id="@+id/expand_button_touch_container" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:minWidth="@dimen/notification_content_margin_end" + > + + <include layout="@layout/notification_2025_expand_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top|end" + /> + + </FrameLayout> + + </LinearLayout> + + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_gravity="top|end" /> + + </com.android.internal.widget.NotificationMaxHeightFrameLayout> + + <LinearLayout + android:id="@+id/notification_action_list_margin_target" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="-20dp" + android:clipChildren="false" + android:orientation="vertical"> + <include layout="@layout/notification_template_smart_reply_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_content_margin" + android:layout_marginStart="@dimen/notification_2025_content_margin_start" + android:layout_marginEnd="@dimen/notification_content_margin_end" /> + <include layout="@layout/notification_material_action_list" /> + </LinearLayout> +</LinearLayout> +</com.android.internal.widget.ConversationLayout> diff --git a/core/res/res/layout/notification_2025_template_conversation.xml b/core/res/res/layout/notification_2025_template_conversation.xml deleted file mode 100644 index 24b6ad09d3f7..000000000000 --- a/core/res/res/layout/notification_2025_template_conversation.xml +++ /dev/null @@ -1,159 +0,0 @@ -<?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 - --> - -<!-- extends FrameLayout --> -<com.android.internal.widget.ConversationLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/status_bar_latest_event_content" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:clipChildren="false" - android:tag="conversation" - android:theme="@style/Theme.DeviceDefault.Notification" - > - - <include layout="@layout/notification_2025_conversation_icon_container" /> - - <!-- Wraps entire "expandable" notification --> - <com.android.internal.widget.RemeasuringLinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="top" - android:clipToPadding="false" - android:clipChildren="false" - android:orientation="vertical" - > - <!-- LinearLayout for Expand Button--> - <com.android.internal.widget.RemeasuringLinearLayout - android:id="@+id/expand_button_and_content_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="start|top" - android:orientation="horizontal" - android:clipChildren="false" - android:clipToPadding="false"> - <!--TODO: move this into a separate layout and share logic with the header to bring back app opps etc--> - <com.android.internal.widget.RemeasuringLinearLayout - android:id="@+id/notification_action_list_margin_target" - android:layout_width="0dp" - android:orientation="vertical" - android:layout_height="wrap_content" - android:layout_weight="1"> - - <!-- Header --> - - <!-- Use layout_marginStart instead of paddingStart to work around strange - measurement behavior on lower display densities. --> - <include - layout="@layout/notification_2025_conversation_header" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="2dp" - android:layout_marginStart="@dimen/notification_2025_content_margin_start" - /> - - <!-- Messages --> - <com.android.internal.widget.MessagingLinearLayout - android:id="@+id/notification_messaging" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="@dimen/notification_text_size" - android:spacing="@dimen/notification_messaging_spacing" - android:clipToPadding="false" - android:clipChildren="false" - /> - </com.android.internal.widget.RemeasuringLinearLayout> - - <!-- This is where the expand button container will be placed when collapsed--> - </com.android.internal.widget.RemeasuringLinearLayout> - - <include layout="@layout/notification_template_smart_reply_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/notification_content_margin" - android:layout_marginStart="@dimen/notification_2025_content_margin_start" - android:layout_marginEnd="@dimen/notification_content_margin_end" /> - <include layout="@layout/notification_material_action_list" /> - </com.android.internal.widget.RemeasuringLinearLayout> - - <!--expand_button_a11y_container ensures talkback focus order is correct when view is expanded. - The -1px of marginTop and 1px of paddingTop make sure expand_button_a11y_container is prior to - its sibling view in accessibility focus order. - {see android.view.ViewGroup.addChildrenForAccessibility()} - expand_button_container will be moved under expand_button_and_content_container when collapsed, - this dynamic movement ensures message can flow under expand button when expanded--> - <FrameLayout - android:id="@+id/expand_button_a11y_container" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="end|top" - android:clipChildren="false" - android:clipToPadding="false" - android:layout_marginTop="-1px" - android:paddingTop="1px" - > - <!--expand_button_container is dynamically placed between here and at the end of the - layout. It starts here since only FrameLayout layout params have gravity--> - <LinearLayout - android:id="@+id/expand_button_container" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:layout_gravity="end|top" - android:clipChildren="false" - android:clipToPadding="false" - android:orientation="vertical"> - <include layout="@layout/notification_close_button" - android:layout_width="@dimen/notification_close_button_size" - android:layout_height="@dimen/notification_close_button_size" - android:layout_gravity="end" - android:layout_marginEnd="20dp" - /> - <!--expand_button_touch_container makes sure that we can nicely center the expand - content in the collapsed layout while the parent makes sure that we're never laid out - bigger than the messaging content.--> - <LinearLayout - android:id="@+id/expand_button_touch_container" - android:layout_width="wrap_content" - android:layout_height="@dimen/conversation_expand_button_height" - android:orientation="horizontal" - android:layout_gravity="end|top" - android:paddingEnd="0dp" - android:clipToPadding="false" - android:clipChildren="false" - > - <!-- Images --> - <com.android.internal.widget.MessagingLinearLayout - android:id="@+id/conversation_image_message_container" - android:forceHasOverlappingRendering="false" - android:layout_width="40dp" - android:layout_height="40dp" - android:layout_marginStart="@dimen/conversation_image_start_margin" - android:spacing="0dp" - android:layout_gravity="center" - android:clipToPadding="false" - android:clipChildren="false" - /> - <include layout="@layout/notification_2025_expand_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="top|end" - /> - </LinearLayout> - </LinearLayout> - </FrameLayout> -</com.android.internal.widget.ConversationLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_call.xml b/core/res/res/layout/notification_2025_template_expanded_call.xml index 0be61253c917..bbc29664d594 100644 --- a/core/res/res/layout/notification_2025_template_expanded_call.xml +++ b/core/res/res/layout/notification_2025_template_expanded_call.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2024 The Android Open Source Project + ~ Copyright (C) 2025 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. @@ -27,83 +27,47 @@ > <!-- CallLayout shares visual appearance with ConversationLayout, so shares layouts --> - <include layout="@layout/notification_2025_conversation_icon_container" /> + <include layout="@layout/notification_2025_conversation_header"/> - <LinearLayout + <com.android.internal.widget.RemeasuringLinearLayout android:id="@+id/notification_action_list_margin_target" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/notification_content_margin" - android:orientation="vertical" - > - - <LinearLayout + android:layout_gravity="top" + android:clipChildren="false" + android:orientation="vertical"> + + <!-- Note: the top margin is being set in code based on the estimated space needed for + the header text. --> + <com.android.internal.widget.RemeasuringLinearLayout + android:id="@+id/notification_main_column" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_gravity="top" android:layout_weight="1" - android:orientation="horizontal" + android:layout_marginStart="@dimen/notification_2025_content_margin_start" + android:layout_marginEnd="@dimen/notification_content_margin_end" + android:orientation="vertical" + android:clipChildren="false" > - <LinearLayout - android:id="@+id/notification_main_column" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/notification_2025_content_margin_start" - android:layout_weight="1" - android:orientation="vertical" - android:minHeight="68dp" - > - - <include - layout="@layout/notification_2025_conversation_header" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - /> - - <include layout="@layout/notification_template_text_multiline" /> - - <include - android:layout_width="match_parent" - android:layout_height="@dimen/notification_progress_bar_height" - android:layout_marginTop="@dimen/notification_progress_margin_top" - layout="@layout/notification_template_progress" - /> - </LinearLayout> - - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:minWidth="@dimen/notification_content_margin_end" - > - - <include - layout="@layout/notification_expand_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - /> + <include layout="@layout/notification_template_part_line1"/> - </FrameLayout> + <include layout="@layout/notification_template_text_multiline" /> - </LinearLayout> + </com.android.internal.widget.RemeasuringLinearLayout> - <ViewStub - android:layout="@layout/notification_material_reply_text" - android:id="@+id/notification_material_reply_container" + <include layout="@layout/notification_template_smart_reply_container" android:layout_width="match_parent" android:layout_height="wrap_content" - /> - - <include - layout="@layout/notification_template_smart_reply_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/notification_2025_content_margin_start" - android:layout_marginEnd="@dimen/notification_content_margin_end" android:layout_marginTop="@dimen/notification_content_margin" - /> + android:layout_marginStart="@dimen/notification_2025_content_margin_start" + android:layout_marginEnd="@dimen/notification_content_margin_end" /> <include layout="@layout/notification_material_action_list" /> - </LinearLayout> + </com.android.internal.widget.RemeasuringLinearLayout> + + <include layout="@layout/notification_template_right_icon" /> </com.android.internal.widget.CallLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_conversation.xml b/core/res/res/layout/notification_2025_template_expanded_conversation.xml new file mode 100644 index 000000000000..d7e8bb3b6da2 --- /dev/null +++ b/core/res/res/layout/notification_2025_template_expanded_conversation.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 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 + --> + +<!-- extends FrameLayout --> +<com.android.internal.widget.ConversationLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/status_bar_latest_event_content" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipToPadding="false" + android:clipChildren="false" + android:tag="conversation" + > + + <include layout="@layout/notification_2025_conversation_header"/> + + <com.android.internal.widget.RemeasuringLinearLayout + android:id="@+id/notification_action_list_margin_target" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:clipChildren="false" + android:orientation="vertical"> + + <!-- Note: the top margin is being set in code based on the estimated space needed for + the header text. --> + <com.android.internal.widget.RemeasuringLinearLayout + android:id="@+id/notification_main_column" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:layout_weight="1" + android:layout_marginEnd="@dimen/notification_content_margin_end" + android:orientation="vertical" + android:clipChildren="false" + > + + <include layout="@layout/notification_template_part_line1"/> + + <com.android.internal.widget.MessagingLinearLayout + android:id="@+id/notification_messaging" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:spacing="@dimen/notification_messaging_spacing" /> + </com.android.internal.widget.RemeasuringLinearLayout> + + <include layout="@layout/notification_template_smart_reply_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_content_margin" + android:layout_marginStart="@dimen/notification_2025_content_margin_start" + android:layout_marginEnd="@dimen/notification_content_margin_end" /> + + <include layout="@layout/notification_material_action_list" /> + + </com.android.internal.widget.RemeasuringLinearLayout> + + <include layout="@layout/notification_template_right_icon" /> + +</com.android.internal.widget.ConversationLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_messaging.xml b/core/res/res/layout/notification_2025_template_expanded_messaging.xml index 177706c6d58d..20abfee6a4b6 100644 --- a/core/res/res/layout/notification_2025_template_expanded_messaging.xml +++ b/core/res/res/layout/notification_2025_template_expanded_messaging.xml @@ -36,17 +36,21 @@ android:clipChildren="false" android:orientation="vertical"> + <!-- Note: the top margin is being set in code based on the estimated space needed for + the header text. --> <com.android.internal.widget.RemeasuringLinearLayout android:id="@+id/notification_main_column" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="top" android:layout_weight="1" - android:layout_marginTop="@dimen/notification_2025_header_height" android:layout_marginEnd="@dimen/notification_content_margin_end" android:orientation="vertical" android:clipChildren="false" > + + <include layout="@layout/notification_template_part_line1"/> + <com.android.internal.widget.MessagingLinearLayout android:id="@+id/notification_messaging" android:layout_width="match_parent" diff --git a/core/res/res/layout/notification_2025_top_line_views.xml b/core/res/res/layout/notification_2025_top_line_views.xml index 74873463391e..7431c421ed3d 100644 --- a/core/res/res/layout/notification_2025_top_line_views.xml +++ b/core/res/res/layout/notification_2025_top_line_views.xml @@ -20,7 +20,7 @@ <merge xmlns:android="http://schemas.android.com/apk/res/android"> - <TextView + <com.android.internal.widget.ObservableTextView android:id="@+id/app_name_text" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -108,6 +108,42 @@ android:visibility="gone" /> + <TextView + android:id="@+id/verification_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:singleLine="true" + android:visibility="gone" + /> + + <ImageView + android:id="@+id/verification_icon" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:baseline="@dimen/notification_2025_badge_baseline" + android:scaleType="fitCenter" + android:visibility="gone" + /> + + <TextView + android:id="@+id/verification_text" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:layout_weight="100" + android:showRelative="true" + android:singleLine="true" + android:visibility="gone" + /> + <ImageButton android:id="@+id/feedback" android:layout_width="@dimen/notification_feedback_size" diff --git a/core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml b/core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml index 149a5a9568f2..2df1725e7234 100644 --- a/core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml +++ b/core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml @@ -45,6 +45,7 @@ android:maxDrawableWidth="@dimen/notification_icon_circle_size" android:maxDrawableHeight="@dimen/notification_icon_circle_size" android:scaleType="centerCrop" + android:visibility="gone" android:importantForAccessibility="no" /> <ViewStub diff --git a/core/res/res/values-w192dp/dimens_watch.xml b/core/res/res/values-w192dp/dimens_watch.xml deleted file mode 100644 index c6bf767ab6b8..000000000000 --- a/core/res/res/values-w192dp/dimens_watch.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2025 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> - <!-- 16.7% of display size --> - <dimen name="base_error_dialog_top_padding">32dp</dimen> - <!-- 5.2% of display size --> - <dimen name="base_error_dialog_padding">10dp</dimen> - <!-- 20.83% of display size --> - <dimen name="base_error_dialog_bottom_padding">40dp</dimen> - - <!-- watch's indeterminate progress bar dimens based on the current screen size --> - <dimen name="loader_horizontal_min_width_watch">67dp</dimen> - <dimen name="loader_horizontal_min_height_watch">15dp</dimen> -</resources> diff --git a/core/res/res/values-w216dp/dimens_watch.xml b/core/res/res/values-w216dp/dimens_watch.xml deleted file mode 100644 index e14ce5e98f55..000000000000 --- a/core/res/res/values-w216dp/dimens_watch.xml +++ /dev/null @@ -1,21 +0,0 @@ -<!-- - ~ Copyright (C) 2025 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> - <!-- watch's indeterminate progress bar dimens based on the current screen size --> - <dimen name="loader_horizontal_min_width_watch">72dp</dimen> - <dimen name="loader_horizontal_min_height_watch">16dp</dimen> -</resources>
\ No newline at end of file diff --git a/core/res/res/values-w228dp/dimens_watch.xml b/core/res/res/values-w228dp/dimens_watch.xml deleted file mode 100644 index 3c6265690c3c..000000000000 --- a/core/res/res/values-w228dp/dimens_watch.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2025 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> - <!-- watch's indeterminate progress bar dimens based on the current screen size --> - <dimen name="loader_horizontal_min_width">76dp</dimen> - <dimen name="loader_horizontal_min_height">14dp</dimen> -</resources> diff --git a/core/res/res/values-w240dp/dimens_material.xml b/core/res/res/values-w240dp/dimens_material.xml index e30aea46aec7..bd26c8bf84df 100644 --- a/core/res/res/values-w240dp/dimens_material.xml +++ b/core/res/res/values-w240dp/dimens_material.xml @@ -21,8 +21,4 @@ <dimen name="screen_percentage_12">28.8dp</dimen> <dimen name="screen_percentage_15">36dp</dimen> <dimen name="screen_percentage_3646">87.5dp</dimen> - - <!-- watch's indeterminate progress bar dimens based on the current screen size --> - <dimen name="progress_indeterminate_horizontal_min_width_watch">80dp</dimen> - <dimen name="progress_indeterminate_horizontal_min_height_watch">17dp</dimen> </resources> diff --git a/core/res/res/values-watch/styles_device_defaults.xml b/core/res/res/values-watch/styles_device_defaults.xml index eeb66e7cf6a8..fb7dbb0660c5 100644 --- a/core/res/res/values-watch/styles_device_defaults.xml +++ b/core/res/res/values-watch/styles_device_defaults.xml @@ -42,8 +42,5 @@ <item name="indeterminateOnly">false</item> <!-- Use Wear Material3 ring shape as default determinate drawable --> <item name="progressDrawable">@drawable/progress_ring_watch</item> - <item name="indeterminateDrawable">@drawable/loader_horizontal_watch</item> - <item name="android:minWidth">@dimen/loader_horizontal_min_width_watch</item> - <item name="android:minHeight">@dimen/loader_horizontal_min_height_watch</item> </style> </resources> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 66111785af4f..d2c993aecb0d 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -6934,6 +6934,8 @@ <enum name="line" value="2" /> <!-- Ring shape. --> <enum name="ring" value="3" /> + <!-- ARC shape. --> + <enum name="arc" value="4"/> </attr> <!-- Inner radius of the ring expressed as a ratio of the ring's width. For instance, if innerRadiusRatio=9, then the inner radius equals the ring's width divided by 9. @@ -6966,6 +6968,12 @@ <attr name="opticalInsetRight" /> <!-- Bottom optical inset. --> <attr name="opticalInsetBottom" /> + <!-- Attributes that customize the stroke line cap. @hide --> + <attr name="strokeCap" format="enum"> + <enum name="butt" value="0"/> + <enum name="round" value="1"/> + <enum name="square" value="2"/> + </attr> </declare-styleable> <!-- Used to specify the size of the shape for GradientDrawable. --> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index f2ec56c69374..59ed25a1865f 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -64,6 +64,13 @@ <integer name="auto_data_switch_performance_stability_time_threshold_millis">120000</integer> <java-symbol type="integer" name="auto_data_switch_performance_stability_time_threshold_millis" /> + <!-- Define the bar for switching data back to the default SIM when both SIMs are out of service + in milliseconds. A value of 0 means an immediate switch, otherwise for a negative value, + the threshold defined by auto_data_switch_availability_stability_time_threshold_millis + will be used instead. --> + <integer name="auto_data_switch_availability_switchback_stability_time_threshold_millis">150000</integer> + <java-symbol type="integer" name="auto_data_switch_availability_switchback_stability_time_threshold_millis" /> + <!-- Define the maximum retry times when a validation for switching failed.--> <integer name="auto_data_switch_validation_max_retry">7</integer> <java-symbol type="integer" name="auto_data_switch_validation_max_retry" /> diff --git a/core/res/res/values/dimens_watch.xml b/core/res/res/values/dimens_watch.xml index 19845916984b..7462b733b0ae 100644 --- a/core/res/res/values/dimens_watch.xml +++ b/core/res/res/values/dimens_watch.xml @@ -52,7 +52,7 @@ <dimen name="primary_content_alpha_device_default">0.38</dimen> <!-- values for wear material3 progress bar(progress indicator) --> - <item name="progressbar_inner_radius_ratio" format="float" type="dimen">2.12</item> + <item name="progressbar_inner_radius_ratio" format="float" type="dimen">2</item> <dimen name="progressbar_thickness">8dp</dimen> <dimen name="progressbar_elevation">0.1dp</dimen> @@ -61,8 +61,4 @@ <dimen name="disabled_alpha_wear_material3">0.12</dimen> <!-- Alpha transparency applied to elements which are considered primary (e.g. primary text) --> <dimen name="primary_content_alpha_wear_material3">0.38</dimen> - - <!-- watch's indeterminate progress bar dimens --> - <dimen name="loader_horizontal_min_width">68dp</dimen> - <dimen name="loader_horizontal_min_height">13dp</dimen> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 26f0ab3f28e1..b013ffd41ecb 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3264,6 +3264,7 @@ <java-symbol type="dimen" name="notification_content_margin" /> <java-symbol type="dimen" name="notification_2025_margin" /> <java-symbol type="dimen" name="notification_2025_content_margin_top" /> + <java-symbol type="dimen" name="notification_2025_content_margin_start" /> <java-symbol type="dimen" name="notification_2025_expand_button_horizontal_icon_padding" /> <java-symbol type="dimen" name="notification_2025_expand_button_reduced_end_padding" /> <java-symbol type="dimen" name="notification_progress_margin_horizontal" /> @@ -4705,7 +4706,8 @@ <java-symbol type="dimen" name="conversation_icon_container_top_padding" /> <java-symbol type="dimen" name="conversation_icon_container_top_padding_small_avatar" /> <java-symbol type="layout" name="notification_template_material_conversation" /> - <java-symbol type="layout" name="notification_2025_template_conversation" /> + <java-symbol type="layout" name="notification_2025_template_collapsed_conversation" /> + <java-symbol type="layout" name="notification_2025_template_expanded_conversation" /> <java-symbol type="dimen" name="button_padding_horizontal_material" /> <java-symbol type="dimen" name="button_inset_horizontal_material" /> <java-symbol type="layout" name="conversation_face_pile_layout" /> diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java index 9383807ec761..8ef105f79988 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java +++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java @@ -56,6 +56,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -116,7 +117,8 @@ public class DisplayManagerGlobalTest { @Test public void testDisplayListenerIsCalled_WhenDisplayEventOccurs() throws RemoteException { mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, - ALL_DISPLAY_EVENTS, /* packageName= */ null); + ALL_DISPLAY_EVENTS, /* packageName= */ null, + /* isEventFilterExplicit */ true); Mockito.verify(mDisplayManager) .registerCallbackWithEventMask(mCallbackCaptor.capture(), anyLong()); IDisplayManagerCallback callback = mCallbackCaptor.getValue(); @@ -151,7 +153,7 @@ public class DisplayManagerGlobalTest { mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE | INTERNAL_EVENT_FLAG_DISPLAY_STATE, - null); + null, /* isEventFilterExplicit */ true); Mockito.verify(mDisplayManager) .registerCallbackWithEventMask(mCallbackCaptor.capture(), anyLong()); IDisplayManagerCallback callback = mCallbackCaptor.getValue(); @@ -172,11 +174,80 @@ public class DisplayManagerGlobalTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_DELAY_IMPLICIT_RR_REGISTRATION_UNTIL_RR_ACCESSED) + public void test_refreshRateRegistration_implicitRRCallbacksEnabled() + throws RemoteException { + // Subscription without supplied events doesn't subscribe to RR events + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + ALL_DISPLAY_EVENTS, /* packageName= */ null, + /* isEventFilterExplicit */ false); + Mockito.verify(mDisplayManager) + .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(ALL_DISPLAY_EVENTS)); + + // After registering to refresh rate changes, subscription without supplied events subscribe + // to RR events + mDisplayManagerGlobal.registerForRefreshRateChanges(); + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + ALL_DISPLAY_EVENTS, /* packageName= */ null, + /* isEventFilterExplicit */ false); + Mockito.verify(mDisplayManager) + .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(ALL_DISPLAY_EVENTS + | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE)); + + // Assert all the existing listeners are also subscribed to RR events + CopyOnWriteArrayList<DisplayManagerGlobal.DisplayListenerDelegate> delegates = + mDisplayManagerGlobal.getDisplayListeners(); + for (DisplayManagerGlobal.DisplayListenerDelegate delegate: delegates) { + assertEquals(ALL_DISPLAY_EVENTS | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, + delegate.mInternalEventFlagsMask); + } + + // Subscription to RR when events are supplied doesn't happen + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + ALL_DISPLAY_EVENTS, /* packageName= */ null, + /* isEventFilterExplicit */ true); + Mockito.verify(mDisplayManager) + .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(ALL_DISPLAY_EVENTS)); + + // Assert one listeners are not subscribed to RR events + delegates = mDisplayManagerGlobal.getDisplayListeners(); + int subscribedListenersCount = 0; + int nonSubscribedListenersCount = 0; + for (DisplayManagerGlobal.DisplayListenerDelegate delegate: delegates) { + + if (delegate.isEventFilterExplicit()) { + assertEquals(ALL_DISPLAY_EVENTS, delegate.mInternalEventFlagsMask); + nonSubscribedListenersCount++; + } else { + assertEquals(ALL_DISPLAY_EVENTS | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, + delegate.mInternalEventFlagsMask); + subscribedListenersCount++; + } + } + + assertEquals(2, subscribedListenersCount); + assertEquals(1, nonSubscribedListenersCount); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_DELAY_IMPLICIT_RR_REGISTRATION_UNTIL_RR_ACCESSED) + public void test_refreshRateRegistration_implicitRRCallbacksDisabled() + throws RemoteException { + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + ALL_DISPLAY_EVENTS, /* packageName= */ null, + /* isEventFilterExplicit */ false); + Mockito.verify(mDisplayManager) + .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(ALL_DISPLAY_EVENTS + | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE)); + } + + @Test public void testDisplayListenerIsNotCalled_WhenClientIsNotSubscribed() throws RemoteException { // First we subscribe to all events in order to test that the subsequent calls to // registerDisplayListener will update the event mask. mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, - ALL_DISPLAY_EVENTS, /* packageName= */ null); + ALL_DISPLAY_EVENTS, /* packageName= */ null, + /* isEventFilterExplicit */ true); Mockito.verify(mDisplayManager) .registerCallbackWithEventMask(mCallbackCaptor.capture(), anyLong()); IDisplayManagerCallback callback = mCallbackCaptor.getValue(); @@ -184,21 +255,24 @@ public class DisplayManagerGlobalTest { int displayId = 1; mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, ALL_DISPLAY_EVENTS - & ~DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED, null); + & ~DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED, null, + /* isEventFilterExplicit */ true); callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_ADDED); waitForHandler(); Mockito.verifyZeroInteractions(mDisplayListener); mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, ALL_DISPLAY_EVENTS - & ~DISPLAY_CHANGE_EVENTS, null); + & ~DISPLAY_CHANGE_EVENTS, null, + /* isEventFilterExplicit */ true); callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_BASIC_CHANGED); waitForHandler(); Mockito.verifyZeroInteractions(mDisplayListener); mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, ALL_DISPLAY_EVENTS - & ~DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, null); + & ~DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, null, + /* isEventFilterExplicit */ true); callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); waitForHandler(); Mockito.verifyZeroInteractions(mDisplayListener); @@ -218,11 +292,34 @@ public class DisplayManagerGlobalTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_DELAY_IMPLICIT_RR_REGISTRATION_UNTIL_RR_ACCESSED) + public void test_registerNativeRefreshRateCallbacks_enablesRRImplicitRegistrations() + throws RemoteException { + // Registering the display listener without supplied events doesn't subscribe to RR events + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + ALL_DISPLAY_EVENTS, /* packageName= */ null, + /* isEventFilterExplicit */ false); + Mockito.verify(mDisplayManager) + .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(ALL_DISPLAY_EVENTS)); + + // Native subscription for refresh rates is done + mDisplayManagerGlobal.registerNativeChoreographerForRefreshRateCallbacks(); + + // Registering the display listener without supplied events subscribe to RR events + mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, + ALL_DISPLAY_EVENTS, /* packageName= */ null, + /* isEventFilterExplicit */ false); + Mockito.verify(mDisplayManager) + .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(ALL_DISPLAY_EVENTS + | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE)); + } + + @Test public void testDisplayManagerGlobalRegistersWithDisplayManager_WhenThereAreListeners() throws RemoteException { mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BRIGHTNESS_CHANGED, - null); + null, /* isEventFilterExplicit */ true); InOrder inOrder = Mockito.inOrder(mDisplayManager); inOrder.verify(mDisplayManager) @@ -260,9 +357,10 @@ public class DisplayManagerGlobalTest { mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, - null /* packageName */); + null /* packageName */, /* isEventFilterExplicit */ true); mDisplayManagerGlobal.registerDisplayListener(mDisplayListener2, mHandler, - DISPLAY_CHANGE_EVENTS, null /* packageName */); + DISPLAY_CHANGE_EVENTS, null /* packageName */, + /* isEventFilterExplicit */ true); mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(321); waitForHandler(); diff --git a/core/tests/coretests/src/android/os/BundleMergerTest.java b/core/tests/coretests/src/android/os/BundleMergerTest.java index 43ed821e647d..53799935e118 100644 --- a/core/tests/coretests/src/android/os/BundleMergerTest.java +++ b/core/tests/coretests/src/android/os/BundleMergerTest.java @@ -18,6 +18,7 @@ package android.os; import static android.os.BundleMerger.STRATEGY_ARRAY_APPEND; import static android.os.BundleMerger.STRATEGY_ARRAY_LIST_APPEND; +import static android.os.BundleMerger.STRATEGY_ARRAY_UNION; import static android.os.BundleMerger.STRATEGY_BOOLEAN_AND; import static android.os.BundleMerger.STRATEGY_BOOLEAN_OR; import static android.os.BundleMerger.STRATEGY_COMPARABLE_MAX; @@ -28,6 +29,7 @@ import static android.os.BundleMerger.STRATEGY_NUMBER_ADD; import static android.os.BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST; import static android.os.BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD; import static android.os.BundleMerger.STRATEGY_REJECT; +import static android.os.BundleMerger.STRATEGY_STRING_APPEND; import static android.os.BundleMerger.merge; import static org.junit.Assert.assertArrayEquals; @@ -204,6 +206,33 @@ public class BundleMergerTest { } @Test + public void testStrategyArrayUnion() throws Exception { + assertArrayEquals(new int[] {}, + (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {}, new int[] {})); + assertArrayEquals(new int[] {10}, + (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {10}, new int[] {})); + assertArrayEquals(new int[] {20}, + (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {}, new int[] {20})); + assertArrayEquals(new int[] {10, 20}, + (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {10}, new int[] {20})); + assertArrayEquals(new int[] {10, 20, 30, 40}, + (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {10, 30}, new int[] {20, 40})); + assertArrayEquals(new int[] {10, 20, 30}, + (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {10, 30}, new int[] {10, 20})); + assertArrayEquals(new int[] {10, 20}, + (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {10, 20}, new int[] {20})); + assertArrayEquals(new String[] {"a", "b"}, + (String[]) merge(STRATEGY_ARRAY_UNION, new String[] {"a"}, new String[] {"b"})); + assertArrayEquals(new String[] {"a", "b", "c"}, + (String[]) merge(STRATEGY_ARRAY_UNION, new String[] {"a", "b"}, + new String[] {"b", "c"})); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_ARRAY_UNION, 10, 20); + }); + } + + @Test public void testStrategyArrayListAppend() throws Exception { assertEquals(arrayListOf(), merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf())); @@ -224,6 +253,18 @@ public class BundleMergerTest { } @Test + public void testStrategyStringAppend() throws Exception { + assertEquals("ab", merge(STRATEGY_STRING_APPEND, "a", "b")); + assertEquals("abc", merge(STRATEGY_STRING_APPEND, "a", "bc")); + assertEquals("abc", merge(STRATEGY_STRING_APPEND, "ab", "c")); + assertEquals("a,b,c,", merge(STRATEGY_STRING_APPEND, "a,", "b,c,")); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_STRING_APPEND, 10, 20); + }); + } + + @Test public void testSetDefaultMergeStrategy() throws Exception { final BundleMerger merger = new BundleMerger(); merger.setDefaultMergeStrategy(STRATEGY_FIRST); diff --git a/core/tests/coretests/src/android/os/PerfettoTraceTest.java b/core/tests/coretests/src/android/os/PerfettoTraceTest.java index c5c2554c2a67..69150150d6f9 100644 --- a/core/tests/coretests/src/android/os/PerfettoTraceTest.java +++ b/core/tests/coretests/src/android/os/PerfettoTraceTest.java @@ -77,6 +77,8 @@ public class PerfettoTraceTest { private static final String TAG = "PerfettoTraceTest"; private static final String FOO = "foo"; private static final String BAR = "bar"; + private static final String TEXT_ABOVE_4K_SIZE = + new String(new char[8192]).replace('\0', 'a'); private static final Category FOO_CATEGORY = new Category(FOO); private static final int MESSAGE = 1234567; @@ -106,8 +108,8 @@ public class PerfettoTraceTest { PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); PerfettoTrace.instant(FOO_CATEGORY, "event") - .addFlow(2) - .addTerminatingFlow(3) + .setFlow(2) + .setTerminatingFlow(3) .addArg("long_val", 10000000000L) .addArg("bool_val", true) .addArg("double_val", 3.14) @@ -155,41 +157,6 @@ public class PerfettoTraceTest { @Test @RequiresFlagsEnabled(android.os.Flags.FLAG_PERFETTO_SDK_TRACING_V2) - public void testDebugAnnotationsWithLambda() throws Exception { - TraceConfig traceConfig = getTraceConfig(FOO); - - PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); - - PerfettoTrace.instant(FOO_CATEGORY, "event").addArg("long_val", 123L).emit(); - - byte[] traceBytes = session.close(); - - Trace trace = Trace.parseFrom(traceBytes); - - boolean hasTrackEvent = false; - boolean hasDebugAnnotations = false; - for (TracePacket packet: trace.getPacketList()) { - TrackEvent event; - if (packet.hasTrackEvent()) { - hasTrackEvent = true; - event = packet.getTrackEvent(); - - if (TrackEvent.Type.TYPE_INSTANT.equals(event.getType()) - && event.getDebugAnnotationsCount() == 1) { - hasDebugAnnotations = true; - - List<DebugAnnotation> annotations = event.getDebugAnnotationsList(); - assertThat(annotations.get(0).getIntValue()).isEqualTo(123L); - } - } - } - - assertThat(hasTrackEvent).isTrue(); - assertThat(hasDebugAnnotations).isTrue(); - } - - @Test - @RequiresFlagsEnabled(android.os.Flags.FLAG_PERFETTO_SDK_TRACING_V2) public void testNamedTrack() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); @@ -440,7 +407,6 @@ public class PerfettoTraceTest { boolean hasTrackEvent = false; boolean hasSourceLocation = false; - for (TracePacket packet: trace.getPacketList()) { TrackEvent event; if (packet.hasTrackEvent()) { @@ -467,6 +433,53 @@ public class PerfettoTraceTest { @Test @RequiresFlagsEnabled(android.os.Flags.FLAG_PERFETTO_SDK_TRACING_V2) + public void testProtoWithSlowPath() throws Exception { + TraceConfig traceConfig = getTraceConfig(FOO); + + PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray()); + + PerfettoTrace.instant(FOO_CATEGORY, "event_proto") + .beginProto() + .beginNested(33L) + .addField(4L, 2L) + .addField(3, TEXT_ABOVE_4K_SIZE) + .endNested() + .addField(2001, "AIDL::IActivityManager") + .endProto() + .emit(); + + byte[] traceBytes = session.close(); + + Trace trace = Trace.parseFrom(traceBytes); + + boolean hasTrackEvent = false; + boolean hasSourceLocation = false; + for (TracePacket packet: trace.getPacketList()) { + TrackEvent event; + if (packet.hasTrackEvent()) { + hasTrackEvent = true; + event = packet.getTrackEvent(); + + if (TrackEvent.Type.TYPE_INSTANT.equals(event.getType()) + && event.hasSourceLocation()) { + SourceLocation loc = event.getSourceLocation(); + if (TEXT_ABOVE_4K_SIZE.equals(loc.getFunctionName()) + && loc.getLineNumber() == 2) { + hasSourceLocation = true; + } + } + } + + collectInternedData(packet); + } + + assertThat(hasTrackEvent).isTrue(); + assertThat(hasSourceLocation).isTrue(); + assertThat(mCategoryNames).contains(FOO); + } + + @Test + @RequiresFlagsEnabled(android.os.Flags.FLAG_PERFETTO_SDK_TRACING_V2) public void testProtoNested() throws Exception { TraceConfig traceConfig = getTraceConfig(FOO); diff --git a/core/tests/coretests/src/android/view/AccessibilityInteractionControllerTest.java b/core/tests/coretests/src/android/view/AccessibilityInteractionControllerTest.java index d91541fbc1fc..d6426b0313d4 100644 --- a/core/tests/coretests/src/android/view/AccessibilityInteractionControllerTest.java +++ b/core/tests/coretests/src/android/view/AccessibilityInteractionControllerTest.java @@ -25,6 +25,7 @@ import android.app.Instrumentation; import android.app.Service; import android.app.UiAutomation; import android.graphics.Rect; +import android.os.Process; import android.os.SystemClock; import android.text.TextUtils; import android.view.accessibility.AccessibilityEvent; @@ -32,6 +33,7 @@ import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityTestActivity; import android.view.accessibility.AccessibilityWindowInfo; +import android.view.accessibility.IWindowSurfaceInfoCallback; import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -47,6 +49,7 @@ import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import java.util.List; import java.util.concurrent.TimeoutException; @@ -136,6 +139,20 @@ public class AccessibilityInteractionControllerTest { } } + @Test + public void getWindowSurfaceInfo_shouldCallCallbackWithWindowSurfaceDataFromVri() + throws Exception { + final ViewRootImpl vri = mButton.getRootView().getViewRootImpl(); + IWindowSurfaceInfoCallback callback = Mockito.mock(IWindowSurfaceInfoCallback.class); + + sInstrumentation.runOnMainSync(() -> + mAccessibilityInteractionController.getWindowSurfaceInfoClientThread(callback)); + sInstrumentation.waitForIdleSync(); + + Mockito.verify(callback).provideWindowSurfaceInfo( + vri.getWindowFlags(), Process.myUid(), vri.getSurfaceControl()); + } + private void launchActivity() { final Object waitObject = new Object(); final int[] location = new int[2]; diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java index e6361e10cfa7..6adceb96d977 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java @@ -230,7 +230,9 @@ public class InsetsSourceConsumerTest { new InsetsSourceControl(ID_STATUS_BAR, statusBars(), mLeash, false /* initialVisible */, new Point(), Insets.NONE), new int[1], hideTypes, new int[1], new int[1]); - assertTrue(mRemoveSurfaceCalled); + if (!android.view.inputmethod.Flags.refactorInsetsController()) { + assertTrue(mRemoveSurfaceCalled); + } assertEquals(0, hideTypes[0]); }); diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index a289df0441e5..c40137f1bd34 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -16,8 +16,6 @@ package android.view; -import static android.app.UiModeManager.MODE_NIGHT_NO; -import static android.app.UiModeManager.MODE_NIGHT_YES; import static android.util.SequenceUtils.getInitSeq; import static android.view.HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING; import static android.view.InputDevice.SOURCE_ROTARY_ENCODER; @@ -69,10 +67,8 @@ import static org.junit.Assume.assumeTrue; import android.annotation.NonNull; import android.app.Instrumentation; import android.app.UiModeManager; -import android.app.UiModeManager.ForceInvertType; import android.content.Context; import android.graphics.ForceDarkType; -import android.graphics.ForceDarkType.ForceDarkTypeDef; import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; import android.os.Binder; @@ -97,12 +93,9 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.compatibility.common.util.ShellIdentityUtils; -import com.android.compatibility.common.util.TestUtils; import com.android.cts.input.BlockingQueueEventVerifier; import com.android.window.flags.Flags; -import com.google.common.truth.Expect; - import org.hamcrest.Matcher; import org.junit.After; import org.junit.AfterClass; @@ -131,8 +124,6 @@ public class ViewRootImplTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Rule - public final Expect mExpect = Expect.create(); private ViewRootImpl mViewRootImpl; private View mView; @@ -1516,34 +1507,49 @@ public class ViewRootImplTest { } @Test - @RequiresFlagsEnabled(FLAG_FORCE_INVERT_COLOR) - public void updateConfiguration_returnsExpectedForceDarkMode() { - waitForSystemNightModeActivated(true); - - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK); - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - - waitForSystemNightModeActivated(false); - - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); - verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false, - UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE); + public void forceInvertOffDarkThemeOff_forceDarkModeDisabled() { + mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR); + ShellIdentityUtils.invokeWithShellPermissions(() -> { + Settings.Secure.putInt( + sContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, + /* value= */ 0 + ); + var uiModeManager = sContext.getSystemService(UiModeManager.class); + uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_NO); + }); + + sInstrumentation.runOnMainSync(() -> + mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()) + ); + + assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.NONE); + } + + @Test + public void forceInvertOnDarkThemeOff_forceDarkModeEnabled() { + mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR); + ShellIdentityUtils.invokeWithShellPermissions(() -> { + Settings.Secure.putInt( + sContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, + /* value= */ 1 + ); + var uiModeManager = sContext.getSystemService(UiModeManager.class); + uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_NO); + }); + + sInstrumentation.runOnMainSync(() -> + mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()) + ); + + assertThat(mViewRootImpl.determineForceDarkType()) + .isEqualTo(ForceDarkType.FORCE_INVERT_COLOR_DARK); } @Test - @EnableFlags(FLAG_FORCE_INVERT_COLOR) public void forceInvertOffForceDarkOff_forceDarkModeDisabled() { + mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR); ShellIdentityUtils.invokeWithShellPermissions(() -> { Settings.Secure.putInt( sContext.getContentResolver(), @@ -1556,14 +1562,15 @@ public class ViewRootImplTest { }); sInstrumentation.runOnMainSync(() -> - mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())); + mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()) + ); assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.NONE); } @Test - @EnableFlags(FLAG_FORCE_INVERT_COLOR) public void forceInvertOffForceDarkOn_forceDarkModeEnabled() { + mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR); ShellIdentityUtils.invokeWithShellPermissions(() -> { Settings.Secure.putInt( sContext.getContentResolver(), @@ -1575,7 +1582,8 @@ public class ViewRootImplTest { }); sInstrumentation.runOnMainSync(() -> - mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())); + mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()) + ); assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.FORCE_DARK); } @@ -1782,39 +1790,4 @@ public class ViewRootImplTest { () -> view.getViewTreeObserver().removeOnDrawListener(listener)); } } - - private void waitForSystemNightModeActivated(boolean active) { - ShellIdentityUtils.invokeWithShellPermissions(() -> - sInstrumentation.runOnMainSync(() -> { - var uiModeManager = sContext.getSystemService(UiModeManager.class); - uiModeManager.setNightModeActivated(active); - })); - sInstrumentation.waitForIdleSync(); - } - - private void verifyForceDarkType(boolean isAppInNightMode, boolean isForceInvertEnabled, - @ForceInvertType int expectedForceInvertType, - @ForceDarkTypeDef int expectedForceDarkType) { - var uiModeManager = sContext.getSystemService(UiModeManager.class); - ShellIdentityUtils.invokeWithShellPermissions(() -> { - uiModeManager.setApplicationNightMode( - isAppInNightMode ? MODE_NIGHT_YES : MODE_NIGHT_NO); - Settings.Secure.putInt( - sContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, - isForceInvertEnabled ? 1 : 0); - }); - - sInstrumentation.runOnMainSync(() -> - mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())); - try { - TestUtils.waitUntil("Waiting for force invert state changed", - () -> (uiModeManager.getForceInvertState() == expectedForceInvertType)); - } catch (Exception e) { - Log.e(TAG, "Unexpected error trying to apply force invert state. " + e); - e.printStackTrace(); - } - - mExpect.that(mViewRootImpl.determineForceDarkType()).isEqualTo(expectedForceDarkType); - } } diff --git a/core/tests/coretests/src/android/view/ViewRootRectTrackerTest.java b/core/tests/coretests/src/android/view/ViewRootRectTrackerTest.java new file mode 100644 index 000000000000..c66e10079bc8 --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewRootRectTrackerTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.testng.AssertJUnit.assertEquals; + +import android.graphics.Rect; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.google.common.collect.Lists; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collections; +import java.util.List; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ViewRootRectTrackerTest { + private ViewRootRectTracker mTracker; + private final List<Rect> mRects = Lists.newArrayList(new Rect(0, 0, 5, 5), + new Rect(5, 5, 10, 10)); + + @Before + public void setUp() { + mTracker = new ViewRootRectTracker(v -> Collections.emptyList()); + } + + @Test + public void setRootRectsAndComputeTest() { + mTracker.setRootRects(mRects); + mTracker.computeChanges(); + assertEquals(mRects, mTracker.getLastComputedRects()); + } + + @Test + public void waitingForComputeChangesTest() { + mTracker.setRootRects(mRects); + assertTrue(mTracker.isWaitingForComputeChanges()); + mTracker.computeChangedRects(); + assertFalse(mTracker.isWaitingForComputeChanges()); + + View mockView = mock(View.class); + mTracker.updateRectsForView(mockView); + assertTrue(mTracker.isWaitingForComputeChanges()); + mTracker.computeChangedRects(); + assertFalse(mTracker.isWaitingForComputeChanges()); + } +} diff --git a/core/tests/coretests/src/com/android/internal/os/KernelSingleUidTimeReaderTest.java b/core/tests/coretests/src/com/android/internal/os/KernelSingleUidTimeReaderTest.java index 3239598eccdc..0ba2d851feb9 100644 --- a/core/tests/coretests/src/com/android/internal/os/KernelSingleUidTimeReaderTest.java +++ b/core/tests/coretests/src/com/android/internal/os/KernelSingleUidTimeReaderTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.annotation.SuppressLint; import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.ravenwood.RavenwoodRule; import android.util.SparseArray; @@ -287,6 +288,7 @@ public class KernelSingleUidTimeReaderTest { 0, lastUidCpuTimes.size()); } + @SuppressLint("CheckResult") @Test public void testAddDeltaFromBpf() { LongArrayMultiStateCounter counter = new LongArrayMultiStateCounter(2, 5); diff --git a/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java b/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java index 7e5d0a4c2e42..959e121aae9e 100644 --- a/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java +++ b/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import android.annotation.SuppressLint; import android.os.BadParcelableException; import android.os.Parcel; import android.platform.test.ravenwood.RavenwoodRule; @@ -176,6 +177,7 @@ public class LongArrayMultiStateCounterTest { assertCounts(newCounter, 0, new long[]{116, 232, 364, 528}); } + @SuppressLint("CheckResult") private void assertCounts(LongArrayMultiStateCounter counter, int state, long[] expected) { long[] counts = new long[expected.length]; counter.getCounts(counts, state); diff --git a/graphics/java/android/framework_graphics.aconfig b/graphics/java/android/framework_graphics.aconfig index a63cbee4d707..fdbee3ccbebe 100644 --- a/graphics/java/android/framework_graphics.aconfig +++ b/graphics/java/android/framework_graphics.aconfig @@ -42,3 +42,11 @@ flag { description: "Add DISPLAY_BT2020 ColorSpace support" bug: "344038816" } + +flag { + name: "gradient_drawable_shape_rounded_cap" + is_fixed_read_only: true + namespace: "core_graphics" + description: "Make GradientDrawable support drawing ring with rounded stroke cap." + bug: "380000245" +} diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java index 79d559bf77b4..3444f84c20af 100644 --- a/graphics/java/android/graphics/HardwareRenderer.java +++ b/graphics/java/android/graphics/HardwareRenderer.java @@ -659,6 +659,13 @@ public class HardwareRenderer { } /** + * @hide + */ + public void addObserver(long nativeObserver) { + nAddObserver(mNativeProxy, nativeObserver); + } + + /** * TODO: Public API this? * * @hide @@ -668,6 +675,13 @@ public class HardwareRenderer { } /** + * @hide + */ + public void removeObserver(long nativeObserver) { + nRemoveObserver(mNativeProxy, nativeObserver); + } + + /** * Sets the desired color mode on this renderer. Whether or not the actual rendering * will use the requested colorMode depends on the hardware support for such rendering. * diff --git a/graphics/java/android/graphics/drawable/GradientDrawable.java b/graphics/java/android/graphics/drawable/GradientDrawable.java index 29d033e64aea..ff1dc93d787b 100644 --- a/graphics/java/android/graphics/drawable/GradientDrawable.java +++ b/graphics/java/android/graphics/drawable/GradientDrawable.java @@ -16,7 +16,11 @@ package android.graphics.drawable; +import static com.android.graphics.flags.Flags.FLAG_GRADIENT_DRAWABLE_SHAPE_ROUNDED_CAP; +import static com.android.graphics.flags.Flags.gradientDrawableShapeRoundedCap; + import android.annotation.ColorInt; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; @@ -125,8 +129,14 @@ public class GradientDrawable extends Drawable { */ public static final int RING = 3; + /** + * Shape is an arc. + */ + @FlaggedApi(FLAG_GRADIENT_DRAWABLE_SHAPE_ROUNDED_CAP) + public static final int ARC = 4; + /** @hide */ - @IntDef({RECTANGLE, OVAL, LINE, RING}) + @IntDef({RECTANGLE, OVAL, LINE, RING, ARC}) @Retention(RetentionPolicy.SOURCE) public @interface Shape {} @@ -167,6 +177,17 @@ public class GradientDrawable extends Drawable { @Retention(RetentionPolicy.SOURCE) public @interface RadiusType {} + private static final int BUTT = 0; + + private static final int ROUND = 1; + + private static final int SQUARE = 2; + + /** @hide */ + @IntDef({BUTT, ROUND, SQUARE}) + @Retention(RetentionPolicy.SOURCE) + public @interface StrokeCap {} + private static final float DEFAULT_INNER_RADIUS_RATIO = 3.0f; private static final float DEFAULT_THICKNESS_RATIO = 9.0f; @@ -191,6 +212,8 @@ public class GradientDrawable extends Drawable { private boolean mMutated; private Path mRingPath; private boolean mPathIsDirty = true; + private Path mArcPath; + private Path mArcOutlinePath; /** Current gradient radius, valid when {@link #mGradientIsDirty} is false. */ private float mGradientRadius; @@ -850,6 +873,55 @@ public class GradientDrawable extends Drawable { } break; } + case ARC: + if (gradientDrawableShapeRoundedCap()) { + // TODO(b/394988176): Consider applying ARC drawing logic to RING shape. + float centerX = mRect.centerX(); + float centerY = mRect.centerY(); + float thickness = + st.mThickness != -1 ? st.mThickness + : mRect.width() / st.mThicknessRatio; + float radius = st.mInnerRadius != -1 ? st.mInnerRadius + : mRect.width() / st.mInnerRadiusRatio; + radius -= thickness; + float sweep = st.mUseLevelForShape ? (360.0f * getLevel() / 10000.0f) : 360f; + mRect.set(centerX - radius, centerY - radius, centerX + radius, + centerY + radius); + + // Prepare paint. Set style to STROKE for purpose of drawing line ARC. + mFillPaint.setStyle(Paint.Style.STROKE); + mFillPaint.setStrokeWidth(thickness); + mFillPaint.setStrokeCap(getStrokeLineCapForPaint(st.mStrokeCap)); + canvas.drawArc(mRect, 0.0f, sweep, /* useCenter= */ false, mFillPaint); + + if (haveStroke) { + if (mArcPath == null) { + mArcPath = new Path(); + } else { + mArcPath.reset(); + } + if (mArcOutlinePath == null) { + mArcOutlinePath = new Path(); + } else { + mArcOutlinePath.reset(); + } + if (sweep == 360f) { + mArcPath.addOval(mRect, Path.Direction.CW); + } else { + mArcPath.arcTo(mRect, 0.0f, sweep, /* forceMoveTo= */ false); + } + + // The arc path doesn't have width. So, to get the outline of the result arc + // shape, we need to apply the paint effect to the path; then use the + // output as the result outline. + mFillPaint.getFillPath(mArcPath, mArcOutlinePath); + canvas.drawPath(mArcOutlinePath, mStrokePaint); + } + + // Restore to FILL + mFillPaint.setStyle(Paint.Style.FILL); + break; + } case RING: Path path = buildRing(st); canvas.drawPath(path, mFillPaint); @@ -993,6 +1065,36 @@ public class GradientDrawable extends Drawable { } /** + * Return current drawable's stroke line cap. Note that this is only respected when drawable is + * {@link Shape#ARC}. + * + * @return the {@link StrokeCap} of current drawable. + * @attr ref android.R.styleable#GradientDrawable_strokeCap + * @see #setStrokeCap(int) + * + * @hide + */ + @StrokeCap + public int getStrokeCap() { + return mGradientState.mStrokeCap; + } + + /** + * Configure the stroke line cap type that drawable will use while drawing. Note that this is + * only respected when drawable is {@link Shape#ARC}. + * + * @param strokeCapType the stroke line cap type that the drawable will use while drawing. + * @attr ref android.R.styleable#GradientDrawable_strokeCap + * @see #getStrokeCap + * + * @hide + */ + public void setStrokeCap(@StrokeCap int strokeCapType) { + mGradientState.mStrokeCap = strokeCapType; + invalidateSelf(); + } + + /** * Configure the padding of the gradient shape * @param left Left padding of the gradient shape * @param top Top padding of the gradient shape @@ -1066,6 +1168,15 @@ public class GradientDrawable extends Drawable { return ringPath; } + private Paint.Cap getStrokeLineCapForPaint(@StrokeCap int strokeLineCap) { + return switch (strokeLineCap) { + case BUTT -> Paint.Cap.BUTT; + case ROUND -> Paint.Cap.ROUND; + case SQUARE -> Paint.Cap.SQUARE; + default -> Paint.Cap.SQUARE; + }; + } + /** * Changes this drawable to use a single color instead of a gradient. * <p> @@ -1484,7 +1595,7 @@ public class GradientDrawable extends Drawable { state.mShape = a.getInt(R.styleable.GradientDrawable_shape, state.mShape); state.mDither = a.getBoolean(R.styleable.GradientDrawable_dither, state.mDither); - if (state.mShape == RING) { + if (state.mShape == RING || state.mShape == ARC) { state.mInnerRadius = a.getDimensionPixelSize( R.styleable.GradientDrawable_innerRadius, state.mInnerRadius); @@ -1503,6 +1614,9 @@ public class GradientDrawable extends Drawable { state.mUseLevelForShape = a.getBoolean( R.styleable.GradientDrawable_useLevel, state.mUseLevelForShape); + + state.mStrokeCap = a.getInt( + R.styleable.GradientDrawable_strokeCap, state.mStrokeCap); } final int tintMode = a.getInt(R.styleable.GradientDrawable_tintMode, -1); @@ -2045,6 +2159,9 @@ public class GradientDrawable extends Drawable { public int mInnerRadius = -1; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050218) public int mThickness = -1; + @UnsupportedAppUsage(trackingBug = 380000245) + @StrokeCap public int mStrokeCap = ROUND; + public boolean mDither = false; public Insets mOpticalInsets = Insets.NONE; diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index a08f88a5b937..1e72d64397d7 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -184,3 +184,13 @@ flag { description: "Try out bubble bar on phones" bug: "394869612" } + +flag { + name: "enable_bubble_task_view_listener" + namespace: "multitasking" + description: "Use the same taskview listener for bubble bar and floating" + bug: "272102927" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml index 95cd1c72a2af..800ea7446b6e 100644 --- a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml +++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml @@ -2,6 +2,7 @@ package="com.android.wm.shell.multivalenttests"> <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/> + <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT"/> <application android:debuggable="true" android:supportsRtl="true" > <uses-library android:name="android.test.runner" /> diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt index 9ebc3d78b3a7..3aefcd5ec6c0 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.bubbles +import android.app.ActivityOptions import android.app.Notification import android.app.PendingIntent import android.content.ComponentName @@ -24,6 +25,8 @@ import android.content.Intent import android.content.pm.ShortcutInfo import android.graphics.drawable.Icon import android.os.UserHandle +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.service.notification.NotificationListenerService.Ranking import android.service.notification.StatusBarNotification import android.view.View @@ -33,6 +36,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_ANYTHING import com.android.wm.shell.R import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener import com.android.wm.shell.common.TestShellExecutor @@ -41,6 +45,7 @@ import com.android.wm.shell.taskview.TaskViewController import com.android.wm.shell.taskview.TaskViewTaskController import com.google.common.truth.Truth.assertThat import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito @@ -48,6 +53,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -61,6 +67,9 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class BubbleTaskViewListenerTest { + @get:Rule + val setFlagsRule = SetFlagsRule() + private val context = ApplicationProvider.getApplicationContext<Context>() private var taskViewController = mock<TaskViewController>() @@ -155,9 +164,22 @@ class BubbleTaskViewListenerTest { } getInstrumentation().waitForIdleSync() - // ..so it's pending intent-based, and launches that + // ..so it's pending intent-based, so the pending intent should be active assertThat(b.isPendingIntentActive).isTrue() - verify(taskViewController).startActivity(any(), eq(pendingIntent), any(), any(), any()) + + val intentCaptor = argumentCaptor<Intent>() + val optionsCaptor = argumentCaptor<ActivityOptions>() + + verify(taskViewController).startActivity(any(), + eq(pendingIntent), + intentCaptor.capture(), + optionsCaptor.capture(), + any()) + val intentFlags = intentCaptor.lastValue.flags + assertThat((intentFlags and Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0).isTrue() + assertThat((intentFlags and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue() + assertThat(optionsCaptor.lastValue.launchedFromBubble).isTrue() + assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() } @Test @@ -178,12 +200,52 @@ class BubbleTaskViewListenerTest { } getInstrumentation().waitForIdleSync() - assertThat(b.isPendingIntentActive).isFalse() - verify(taskViewController).startShortcutActivity(any(), eq(shortcutInfo), any(), any()) + val optionsCaptor = argumentCaptor<ActivityOptions>() + + assertThat(b.isPendingIntentActive).isFalse() // not triggered for shortcut chats + verify(taskViewController).startShortcutActivity(any(), + eq(shortcutInfo), + optionsCaptor.capture(), + any()) + assertThat(optionsCaptor.lastValue.launchedFromBubble).isTrue() + assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isTrue() + assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() } + @EnableFlags(FLAG_ENABLE_BUBBLE_ANYTHING) @Test - fun onInitialized_appBubble() { + fun onInitialized_shortcutBubble() { + val shortcutInfo = ShortcutInfo.Builder(context) + .setId("mockShortcutId") + .build() + + val b = createShortcutBubble(shortcutInfo) + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isChat).isFalse() + assertThat(b.isShortcut).isTrue() + assertThat(b.shortcutInfo).isNotNull() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + val optionsCaptor = argumentCaptor<ActivityOptions>() + + assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active + verify(taskViewController).startShortcutActivity(any(), + eq(shortcutInfo), + optionsCaptor.capture(), + any()) + assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only + assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only + assertThat(optionsCaptor.lastValue.isApplyMultipleTaskFlagForShortcut).isTrue() + assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() + } + + @Test + fun onInitialized_appBubble_intent() { val b = createAppBubble() bubbleTaskViewListener.setBubble(b) @@ -194,11 +256,83 @@ class BubbleTaskViewListenerTest { } getInstrumentation().waitForIdleSync() - assertThat(b.isPendingIntentActive).isFalse() - verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any()) + val intentCaptor = argumentCaptor<Intent>() + val optionsCaptor = argumentCaptor<ActivityOptions>() + + assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active + verify(taskViewController).startActivity(any(), + any(), + intentCaptor.capture(), + optionsCaptor.capture(), + any()) + + assertThat((intentCaptor.lastValue.flags + and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue() + assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only + assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only + assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() } @Test + fun onInitialized_appBubble_pendingIntent() { + val b = createAppBubble(usePendingIntent = true) + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isApp).isTrue() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + val intentCaptor = argumentCaptor<Intent>() + val optionsCaptor = argumentCaptor<ActivityOptions>() + + assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active + verify(taskViewController).startActivity(any(), + any(), + intentCaptor.capture(), + optionsCaptor.capture(), + any()) + + assertThat((intentCaptor.lastValue.flags + and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue() + assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only + assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only + assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() + } + + @Test + fun onInitialized_noteBubble() { + val b = createNoteBubble() + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isNote).isTrue() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + val intentCaptor = argumentCaptor<Intent>() + val optionsCaptor = argumentCaptor<ActivityOptions>() + + assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active + verify(taskViewController).startActivity(any(), + any(), + intentCaptor.capture(), + optionsCaptor.capture(), + any()) + + assertThat((intentCaptor.lastValue.flags + and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue() + assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only + assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only + assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() + } + + + @Test fun onInitialized_preparingTransition() { val b = createAppBubble() bubbleTaskViewListener.setBubble(b) @@ -416,13 +550,24 @@ class BubbleTaskViewListenerTest { assertThat(isNew).isTrue() } - private fun createAppBubble(): Bubble { + private fun createAppBubble(usePendingIntent: Boolean = false): Bubble { val target = Intent(context, TestActivity::class.java) target.setPackage(context.packageName) + if (usePendingIntent) { + // Robolectric doesn't seem to play nice with PendingIntents, have to mock it. + val pendingIntent = mock<PendingIntent>() + whenever(pendingIntent.intent).thenReturn(target) + return Bubble.createAppBubble(pendingIntent, mock<UserHandle>(), + mainExecutor, bgExecutor) + } return Bubble.createAppBubble(target, mock<UserHandle>(), mock<Icon>(), mainExecutor, bgExecutor) } + private fun createShortcutBubble(shortcutInfo: ShortcutInfo): Bubble { + return Bubble.createShortcutBubble(shortcutInfo, mainExecutor, bgExecutor) + } + private fun createNoteBubble(): Bubble { val target = Intent(context, TestActivity::class.java) target.setPackage(context.packageName) diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index f5f3f0fe52eb..a0c68ad44379 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -670,7 +670,4 @@ <dimen name="desktop_windowing_education_promo_height">352dp</dimen> <!-- The corner radius of the desktop windowing education promo. --> <dimen name="desktop_windowing_education_promo_corner_radius">28dp</dimen> - - <!-- The corner radius of freeform tasks in desktop windowing. --> - <dimen name="desktop_windowing_freeform_rounded_corner_radius">16dp</dimen> </resources> diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml index 11a6f32d7454..23c9caf2046c 100644 --- a/libs/WindowManager/Shell/shared/res/values/dimen.xml +++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml @@ -46,4 +46,7 @@ <dimen name="drop_target_expanded_view_height">578</dimen> <dimen name="drop_target_expanded_view_padding_bottom">108</dimen> <dimen name="drop_target_expanded_view_padding_horizontal">24</dimen> + + <!-- The corner radius of freeform tasks in desktop windowing. --> + <dimen name="desktop_windowing_freeform_rounded_corner_radius">16dp</dimen> </resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt index 14338a49ee2f..0e4a6b9fb083 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt @@ -16,12 +16,14 @@ package com.android.wm.shell.shared.desktopmode +import android.Manifest.permission.SYSTEM_ALERT_WINDOW import android.app.TaskInfo import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED import android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION import android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS +import android.content.pm.PackageManager import android.window.DesktopModeFlags import com.android.internal.R @@ -32,8 +34,10 @@ import com.android.internal.R class DesktopModeCompatPolicy(private val context: Context) { private val systemUiPackage: String = context.resources.getString(R.string.config_systemUi) + private val pkgManager: PackageManager + get() = context.getPackageManager() private val defaultHomePackage: String? - get() = context.getPackageManager().getHomeActivities(ArrayList())?.packageName + get() = pkgManager.getHomeActivities(ArrayList())?.packageName /** * If the top activity should be exempt from desktop windowing and forced back to fullscreen. @@ -47,11 +51,12 @@ class DesktopModeCompatPolicy(private val context: Context) { fun isTopActivityExemptFromDesktopWindowing(packageName: String?, numActivities: Int, isTopActivityNoDisplay: Boolean, isActivityStackTransparent: Boolean) = - DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue - && ((isSystemUiTask(packageName) - || isPartOfDefaultHomePackageOrNoHomeAvailable(packageName) - || isTransparentTask(isActivityStackTransparent, numActivities)) - && !isTopActivityNoDisplay) + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue && + ((isSystemUiTask(packageName) || + isPartOfDefaultHomePackageOrNoHomeAvailable(packageName) || + (isTransparentTask(isActivityStackTransparent, numActivities) && + hasFullscreenTransparentPermission(packageName))) && + !isTopActivityNoDisplay) /** * Whether the caption insets should be excluded from configuration for system to handle. @@ -83,6 +88,26 @@ class DesktopModeCompatPolicy(private val context: Context) { private fun isSystemUiTask(packageName: String?) = packageName == systemUiPackage + // Checks if the app for the given package has the SYSTEM_ALERT_WINDOW permission. + private fun hasFullscreenTransparentPermission(packageName: String?): Boolean { + if (DesktopModeFlags.ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS.isTrue) { + if (packageName == null) { + return false + } + return try { + val packageInfo = pkgManager.getPackageInfo( + packageName, + PackageManager.GET_PERMISSIONS + ) + packageInfo?.requestedPermissions?.contains(SYSTEM_ALERT_WINDOW) == true + } catch (e: PackageManager.NameNotFoundException) { + false // Package not found + } + } + // If the flag is disabled we make this condition neutral. + return true + } + /** * Returns true if the tasks base activity is part of the default home package, or there is * currently no default home package available. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index 5bd8d86f1144..0f1bf5e09751 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -15,6 +15,8 @@ */ package com.android.wm.shell.bubbles; +import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; + import android.annotation.DrawableRes; import android.annotation.Nullable; import android.content.Context; @@ -35,7 +37,6 @@ import android.widget.ImageView; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.launcher3.icons.DotRenderer; -import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; import com.android.wm.shell.shared.animation.Interpolators; @@ -132,7 +133,7 @@ public class BadgedImageView extends ConstraintLayout { private void getOutline(Outline outline) { final int bubbleSize = mPositioner.getBubbleSize(); - final int normalizedSize = IconNormalizer.getNormalizedCircleSize(bubbleSize); + final int normalizedSize = Math.round(ICON_VISIBLE_AREA_FACTOR * bubbleSize); final int inset = (bubbleSize - normalizedSize) / 2; outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index d9489287ff42..313d151aeab7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -364,7 +364,7 @@ public class Bubble implements BubbleViewProvider { @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { return new Bubble(intent, user, - /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user), + /* key= */ getAppBubbleKeyForApp(intent.getIntent().getPackage(), user), mainExecutor, bgExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index f6a2c8d9695e..305fcdd5fb7d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -912,7 +912,7 @@ public class BubbleController implements ConfigurationChangeListener, // TODO(b/393172431) : Utilise DragZoneFactory once it is ready final int bubbleBarDropZoneSideSize = getContext().getResources().getDimensionPixelSize( R.dimen.bubble_bar_drop_zone_side_size); - int top = t - bubbleBarDropZoneSideSize; + int top = b - bubbleBarDropZoneSideSize; result.put(BubbleBarLocation.LEFT, new Rect(l, top, l + bubbleBarDropZoneSideSize, b)); result.put(BubbleBarLocation.RIGHT, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 3f607a9c52ef..2c2451cab999 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -197,6 +197,8 @@ public class BubbleExpandedView extends LinearLayout { */ private final FrameLayout mExpandedViewContainer = new FrameLayout(getContext()); + private TaskView.Listener mCurrentTaskViewListener; + private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { private boolean mInitialized = false; private boolean mDestroyed = false; @@ -235,18 +237,24 @@ public class BubbleExpandedView extends LinearLayout { Context context = mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); + Intent fillInIntent = new Intent(); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); PendingIntent pi = PendingIntent.getActivity( context, /* requestCode= */ 0, - mBubble.getIntent().addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), - PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT, + mBubble.getIntent(), + // Needs to be mutable for the fillInIntent + PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT, /* options= */ null); - mTaskView.startActivity(pi, /* fillInIntent= */ null, options, - launchBounds); + mTaskView.startActivity(pi, fillInIntent, options, launchBounds); } else if (!mIsOverflow && isShortcutBubble) { ProtoLog.v(WM_SHELL_BUBBLES, "startingShortcutBubble=%s", getBubbleKey()); - options.setLaunchedFromBubble(true); - options.setApplyActivityFlagsForBubbles(true); + if (mBubble.isChat()) { + options.setLaunchedFromBubble(true); + options.setApplyActivityFlagsForBubbles(true); + } else { + options.setApplyMultipleTaskFlagForShortcut(true); + } mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), options, launchBounds); } else { @@ -453,7 +461,34 @@ public class BubbleExpandedView extends LinearLayout { mTaskView = bubbleTaskView.getTaskView(); // reset the insets that might left after TaskView is shown in BubbleBarExpandedView mTaskView.setCaptionInsets(null); - bubbleTaskView.setDelegateListener(mTaskViewListener); + if (Flags.enableBubbleTaskViewListener()) { + mCurrentTaskViewListener = new BubbleTaskViewListener(mContext, bubbleTaskView, + /* viewParent= */ this, expandedViewManager, + new BubbleTaskViewListener.Callback() { + @Override + public void onTaskCreated() { + setContentVisibility(true); + } + + @Override + public void onContentVisibilityChanged(boolean visible) { + setContentVisibility(visible); + } + + @Override + public void onBackPressed() { + mStackView.onBackPressed(); + } + + @Override + public void onTaskRemovalStarted() { + // nothing to do / handled in listener. + } + }); + } else { + mCurrentTaskViewListener = mTaskViewListener; + bubbleTaskView.setDelegateListener(mCurrentTaskViewListener); + } // set a fixed width so it is not recalculated as part of a rotation. the width will be // updated manually after the rotation. @@ -464,9 +499,12 @@ public class BubbleExpandedView extends LinearLayout { } mExpandedViewContainer.addView(mTaskView, lp); bringChildToFront(mTaskView); - if (bubbleTaskView.isCreated()) { - mTaskViewListener.onTaskCreated( - bubbleTaskView.getTaskId(), bubbleTaskView.getComponentName()); + + if (!Flags.enableBubbleTaskViewListener()) { + if (bubbleTaskView.isCreated()) { + mCurrentTaskViewListener.onTaskCreated( + bubbleTaskView.getTaskId(), bubbleTaskView.getComponentName()); + } } } } @@ -897,7 +935,12 @@ public class BubbleExpandedView extends LinearLayout { Log.w(TAG, "Stack is null for bubble: " + bubble); return; } - boolean isNew = mBubble == null || didBackingContentChange(bubble); + boolean isNew; + if (mCurrentTaskViewListener instanceof BubbleTaskViewListener) { + isNew = ((BubbleTaskViewListener) mCurrentTaskViewListener).setBubble(bubble); + } else { + isNew = mBubble == null || didBackingContentChange(bubble); + } boolean isUpdate = bubble != null && mBubble != null && bubble.getKey().equals(mBubble.getKey()); ProtoLog.d(WM_SHELL_BUBBLES, "BubbleExpandedView - update bubble=%s; isNew=%b; isUpdate=%b", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 221c9332711e..33f1b94bac73 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -16,6 +16,7 @@ package com.android.wm.shell.bubbles; +import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.content.Context; @@ -31,7 +32,6 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.ProtoLog; -import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; @@ -557,8 +557,7 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider { public float getPointerPosition(float bubblePosition) { // TODO: I don't understand why it works but it does - why normalized in portrait // & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation? - final float normalizedSize = IconNormalizer.getNormalizedCircleSize( - getBubbleSize()); + final float normalizedSize = Math.round(ICON_VISIBLE_AREA_FACTOR * getBubbleSize()); return showBubblesVertically() ? bubblePosition + (getBubbleSize() / 2f) : bubblePosition + (normalizedSize / 2f) - mPointerWidth; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java index a38debb702dc..63d713495177 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java @@ -129,27 +129,28 @@ public class BubbleTaskViewListener implements TaskView.Listener { Context context = mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); - Intent fillInIntent = null; + Intent fillInIntent = new Intent(); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); // First try get pending intent from the bubble PendingIntent pi = mBubble.getPendingIntent(); if (pi == null) { - // If null - create new one + // If null - create new one based on the bubble intent pi = PendingIntent.getActivity( context, /* requestCode= */ 0, - mBubble.getIntent() - .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), - PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_UPDATE_CURRENT, + mBubble.getIntent(), + // Needs to be mutable for the fillInIntent + PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT, /* options= */ null); - } else { - fillInIntent = new Intent(pi.getIntent()); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); } mTaskView.startActivity(pi, fillInIntent, options, launchBounds); } else if (isShortcutBubble) { - options.setLaunchedFromBubble(true); - options.setApplyActivityFlagsForBubbles(true); + if (mBubble.isChat()) { + options.setLaunchedFromBubble(true); + options.setApplyActivityFlagsForBubbles(true); + } else { + options.setApplyMultipleTaskFlagForShortcut(true); + } mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), options, launchBounds); } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index a676f41baafe..338ffe76e6ea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.View.INVISIBLE; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE; @@ -325,7 +326,7 @@ public class BubbleTransitions { for (int i = 0; i < info.getChanges().size(); ++i) { final TransitionInfo.Change chg = info.getChanges().get(i); if (chg.getTaskInfo() == null) continue; - if (chg.getMode() != TRANSIT_CHANGE) continue; + if (chg.getMode() != TRANSIT_CHANGE && chg.getMode() != TRANSIT_TO_FRONT) continue; if (!mTaskInfo.token.equals(chg.getTaskInfo().token)) continue; mStartBounds.set(chg.getStartAbsBounds()); // Converting a task into taskview, so treat as "new" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index e3b0872df593..29837dc04423 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -135,6 +135,7 @@ public class BubbleBarLayerView extends FrameLayout /** Shows the expanded view drop target at the requested {@link BubbleBarLocation location} */ public void showBubbleBarExtendedViewDropTarget(@NonNull BubbleBarLocation bubbleBarLocation) { + setVisibility(VISIBLE); mBubbleExpandedViewPinController.showDropTarget(bubbleBarLocation); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index 8377a35a9e7d..87a4115ccd3a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -324,8 +324,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } applyVisibilityToLeash(imeSourceControl); } - if (!mImeShowing) { - removeImeSurface(mDisplayId); + if (!android.view.inputmethod.Flags.refactorInsetsController()) { + if (!mImeShowing) { + removeImeSurface(mDisplayId); + } } } } else { @@ -663,7 +665,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged ImeTracker.forLogging().onProgress(mStatsToken, ImeTracker.PHASE_WM_ANIMATION_RUNNING); t.hide(animatingLeash); - removeImeSurface(mDisplayId); + if (!android.view.inputmethod.Flags.refactorInsetsController()) { + removeImeSurface(mDisplayId); + } if (android.view.inputmethod.Flags.refactorInsetsController()) { setVisibleDirectly(false /* visible */, statsToken); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt new file mode 100644 index 000000000000..7a5bc1383ccf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2025 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.common + +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.RectF +import android.view.SurfaceControl +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.shared.annotations.ShellDesktopThread + +/** + * Controller to manage the indicators that show users the current position of the dragged window on + * the new display when performing drag move across displays. + */ +class MultiDisplayDragMoveIndicatorController( + private val displayController: DisplayController, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val indicatorSurfaceFactory: MultiDisplayDragMoveIndicatorSurface.Factory, + @ShellDesktopThread private val desktopExecutor: ShellExecutor, +) { + @ShellDesktopThread + private val dragIndicators = + mutableMapOf<Int, MutableMap<Int, MultiDisplayDragMoveIndicatorSurface>>() + + /** + * Called during drag move, which started at [startDisplayId]. Updates the position and + * visibility of the drag move indicators for the [taskInfo] based on [boundsDp] on the + * destination displays ([displayIds]) as the dragged window moves. [transactionSupplier] + * provides a [SurfaceControl.Transaction] for applying changes to the indicator surfaces. + * + * It is executed on the [desktopExecutor] to prevent blocking the main thread and avoid jank, + * as creating and manipulating surfaces can be expensive. + */ + fun onDragMove( + boundsDp: RectF, + startDisplayId: Int, + taskInfo: RunningTaskInfo, + displayIds: Set<Int>, + transactionSupplier: () -> SurfaceControl.Transaction, + ) { + desktopExecutor.execute { + for (displayId in displayIds) { + if (displayId == startDisplayId) { + // No need to render indicators on the original display where the drag started. + continue + } + val displayLayout = displayController.getDisplayLayout(displayId) ?: continue + val shouldBeVisible = + RectF.intersects(RectF(boundsDp), displayLayout.globalBoundsDp()) + if ( + dragIndicators[taskInfo.taskId]?.containsKey(displayId) != true && + !shouldBeVisible + ) { + // Skip this display if: + // - It doesn't have an existing indicator that needs to be updated, AND + // - The latest dragged window bounds don't intersect with this display. + continue + } + + val boundsPx = + MultiDisplayDragMoveBoundsCalculator.convertGlobalDpToLocalPxForRect( + boundsDp, + displayLayout, + ) + + // Get or create the inner map for the current task. + val dragIndicatorsForTask = + dragIndicators.getOrPut(taskInfo.taskId) { mutableMapOf() } + dragIndicatorsForTask[displayId]?.also { existingIndicator -> + val transaction = transactionSupplier() + existingIndicator.relayout(boundsPx, transaction, shouldBeVisible) + transaction.apply() + } ?: run { + val newIndicator = + indicatorSurfaceFactory.create( + taskInfo, + displayController.getDisplay(displayId), + ) + newIndicator.show( + transactionSupplier(), + taskInfo, + rootTaskDisplayAreaOrganizer, + displayId, + boundsPx, + ) + dragIndicatorsForTask[displayId] = newIndicator + } + } + } + } + + /** + * Called when the drag ends. Disposes of the drag move indicator surfaces associated with the + * given [taskId]. [transactionSupplier] provides a [SurfaceControl.Transaction] for applying + * changes to the indicator surfaces. + * + * It is executed on the [desktopExecutor] to ensure that any pending `onDragMove` operations + * have completed before disposing of the surfaces. + */ + fun onDragEnd(taskId: Int, transactionSupplier: () -> SurfaceControl.Transaction) { + desktopExecutor.execute { + dragIndicators.remove(taskId)?.values?.takeIf { it.isNotEmpty() }?.let { indicators -> + val transaction = transactionSupplier() + indicators.forEach { indicator -> + indicator.disposeSurface(transaction) + } + transaction.apply() + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt new file mode 100644 index 000000000000..d05d3b0903d7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2025 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.common + +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.graphics.Color +import android.graphics.Rect +import android.os.Trace +import android.view.Display +import android.view.SurfaceControl +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.ui.graphics.toArgb +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.Theme + +/** + * Represents the indicator surface that visualizes the current position of a dragged window during + * a multi-display drag operation. + * + * This class manages the creation, display, and manipulation of the [SurfaceControl]s that act as a + * visual indicator, providing feedback to the user about the dragged window's location. + */ +class MultiDisplayDragMoveIndicatorSurface( + context: Context, + taskInfo: RunningTaskInfo, + display: Display, + surfaceControlBuilderFactory: Factory.SurfaceControlBuilderFactory, +) { + private var isVisible = false + + // A container surface to host the veil background + private var veilSurface: SurfaceControl? = null + + private val decorThemeUtil = DecorThemeUtil(context) + private val lightColors = dynamicLightColorScheme(context) + private val darkColors = dynamicDarkColorScheme(context) + + init { + Trace.beginSection("DragIndicatorSurface#init") + + val displayId = display.displayId + veilSurface = + surfaceControlBuilderFactory + .create("Drag indicator veil of Task=${taskInfo.taskId} Display=$displayId") + .setColorLayer() + .setCallsite("DragIndicatorSurface#init") + .setHidden(true) + .build() + + // TODO: b/383069173 - Add icon for the surface. + + Trace.endSection() + } + + /** + * Disposes the indicator surface using the provided [transaction]. + */ + fun disposeSurface(transaction: SurfaceControl.Transaction) { + veilSurface?.let { veil -> transaction.remove(veil) } + veilSurface = null + } + + /** + * Shows the indicator surface at [bounds] on the specified display ([displayId]), + * visualizing the drag of the [taskInfo]. The indicator surface is shown using [transaction], + * and the [rootTaskDisplayAreaOrganizer] is used to reparent the surfaces. + */ + fun show( + transaction: SurfaceControl.Transaction, + taskInfo: RunningTaskInfo, + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + displayId: Int, + bounds: Rect, + ) { + val backgroundColor = + when (decorThemeUtil.getAppTheme(taskInfo)) { + Theme.LIGHT -> lightColors.surfaceContainer + Theme.DARK -> darkColors.surfaceContainer + } + val veil = veilSurface ?: return + isVisible = true + + rootTaskDisplayAreaOrganizer.reparentToDisplayArea(displayId, veil, transaction) + relayout(bounds, transaction, shouldBeVisible = true) + transaction.show(veil).setColor(veil, Color.valueOf(backgroundColor.toArgb()).components) + transaction.apply() + } + + /** + * Repositions and resizes the indicator surface based on [bounds] using [transaction]. The + * [shouldBeVisible] flag indicates whether the indicator is within the display after relayout. + */ + fun relayout(bounds: Rect, transaction: SurfaceControl.Transaction, shouldBeVisible: Boolean) { + if (!isVisible && !shouldBeVisible) { + // No need to relayout if the surface is already invisible and should not be visible. + return + } + isVisible = shouldBeVisible + val veil = veilSurface ?: return + transaction.setCrop(veil, bounds) + } + + /** + * Factory for creating [MultiDisplayDragMoveIndicatorSurface] instances with the [context]. + */ + class Factory(private val context: Context) { + private val surfaceControlBuilderFactory: SurfaceControlBuilderFactory = + object : SurfaceControlBuilderFactory {} + + /** + * Creates a new [MultiDisplayDragMoveIndicatorSurface] instance to visualize the drag + * operation of the [taskInfo] on the given [display]. + */ + fun create( + taskInfo: RunningTaskInfo, + display: Display, + ) = MultiDisplayDragMoveIndicatorSurface( + context, + taskInfo, + display, + surfaceControlBuilderFactory, + ) + + /** + * Interface for creating [SurfaceControl.Builder] instances. + * + * This provides an abstraction over [SurfaceControl.Builder] creation for testing purposes. + */ + interface SurfaceControlBuilderFactory { + fun create(name: String): SurfaceControl.Builder { + return SurfaceControl.Builder().setName(name) + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java index 8e026f04ac31..04e8d8dee520 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java @@ -24,6 +24,7 @@ import android.content.pm.ActivityInfo; import android.content.res.Resources; import android.graphics.Rect; import android.util.DisplayMetrics; +import android.util.Rational; import android.util.Size; import android.view.Gravity; @@ -41,9 +42,6 @@ public class PipBoundsAlgorithm { private static final String TAG = PipBoundsAlgorithm.class.getSimpleName(); private static final float INVALID_SNAP_FRACTION = -1f; - // The same value (with the same name) is used in Launcher. - private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.01f; - @NonNull private final PipBoundsState mPipBoundsState; @NonNull protected final PipDisplayLayoutState mPipDisplayLayoutState; @NonNull protected final SizeSpecSource mSizeSpecSource; @@ -223,9 +221,8 @@ public class PipBoundsAlgorithm { + " than destination(%s)", sourceRectHint, destinationBounds); return false; } - final float reportedRatio = destinationBounds.width() / (float) destinationBounds.height(); - final float inferredRatio = sourceRectHint.width() / (float) sourceRectHint.height(); - if (Math.abs(reportedRatio - inferredRatio) > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) { + if (!PictureInPictureParams.isSameAspectRatio(sourceRectHint, + new Rational(destinationBounds.width(), destinationBounds.height()))) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "isSourceRectHintValidForEnterPip=false, hint(%s) does not match" + " destination(%s) aspect ratio", sourceRectHint, destinationBounds); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OffscreenTouchZone.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OffscreenTouchZone.java index 381f0b037023..3211307c6f9b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OffscreenTouchZone.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OffscreenTouchZone.java @@ -56,6 +56,7 @@ public class OffscreenTouchZone { /** The function that will be run when this zone is tapped. */ private final Runnable mOnClickRunnable; private SurfaceControlViewHost mViewHost; + private SurfaceControl mLeash; /** * @param isTopLeft Whether the desired touch zone will be on the top/left or the bottom/right @@ -96,6 +97,7 @@ public class OffscreenTouchZone { .setCallsite("OffscreenTouchZone::init"); builder.setParent(stageRoot); SurfaceControl leash = builder.build(); + mLeash = leash; // Create a ViewHost that will hold our view. WindowlessWindowManager wwm = new WindowlessWindowManager(config, leash, null); @@ -117,10 +119,14 @@ public class OffscreenTouchZone { } /** Releases the touch zone when it's no longer needed. */ - void release() { + void release(SurfaceControl.Transaction t) { if (mViewHost != null) { mViewHost.release(); } + if (mLeash != null) { + t.remove(mLeash); + mLeash = null; + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 708e26cc5546..720e8e53b218 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -461,7 +461,14 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return; } - mOffscreenTouchZones.forEach(OffscreenTouchZone::release); + // TODO (b/349828130): It would be good to reuse a Transaction from StageCoordinator's + // mTransactionPool here, but passing it through SplitLayout and specifically + // SplitLayout.release() is complicated because that function is purposely called with a + // null value sometimes. When that function is refactored, we should also pass the + // Transaction in here. + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mOffscreenTouchZones.forEach(touchZone -> touchZone.release(t)); + t.apply(); mOffscreenTouchZones.clear(); } @@ -975,8 +982,16 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange final boolean shouldVeil = insets.left != 0 || insets.top != 0 || insets.right != 0 || insets.bottom != 0; + // Find the "left/top"-most position of the app surface -- usually 0, but sometimes negative + // if the left/top app is offscreen. + int leftTop = 0; + if (Flags.enableFlexibleTwoAppSplit()) { + leftTop = mIsLeftRightSplit ? getTopLeftBounds().left : getTopLeftBounds().top; + } + final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( - mIsLeftRightSplit ? getBottomRightBounds().width() : getBottomRightBounds().height() + leftTop + (mIsLeftRightSplit + ? getBottomRightBounds().width() : getBottomRightBounds().height()) ).position; final Rect endBounds1 = new Rect(); final Rect endBounds2 = new Rect(); 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 2fd8c27d5970..5a246e7c99b9 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 @@ -68,6 +68,8 @@ import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.LaunchAdjacentController; +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController; +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorSurface; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -82,6 +84,7 @@ import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopDisplayEventHandler; +import com.android.wm.shell.desktopmode.DesktopDisplayModeController; import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.desktopmode.DesktopMinimizationTransitionHandler; import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler; @@ -989,7 +992,8 @@ public abstract class WMShellModule { WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, - DesktopTilingDecorViewModel desktopTilingDecorViewModel + DesktopTilingDecorViewModel desktopTilingDecorViewModel, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -1006,7 +1010,30 @@ public abstract class WMShellModule { windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, - desktopTilingDecorViewModel)); + desktopTilingDecorViewModel, + multiDisplayDragMoveIndicatorController)); + } + + @WMSingleton + @Provides + static MultiDisplayDragMoveIndicatorController + providesMultiDisplayDragMoveIndicatorController( + DisplayController displayController, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + MultiDisplayDragMoveIndicatorSurface.Factory + multiDisplayDragMoveIndicatorSurfaceFactory, + @ShellDesktopThread ShellExecutor desktopExecutor + ) { + return new MultiDisplayDragMoveIndicatorController( + displayController, rootTaskDisplayAreaOrganizer, + multiDisplayDragMoveIndicatorSurfaceFactory, desktopExecutor); + } + + @WMSingleton + @Provides + static MultiDisplayDragMoveIndicatorSurface.Factory + providesMultiDisplayDragMoveIndicatorSurfaceFactory(Context context) { + return new MultiDisplayDragMoveIndicatorSurface.Factory(context); } @WMSingleton @@ -1230,13 +1257,10 @@ public abstract class WMShellModule { static Optional<DesktopDisplayEventHandler> provideDesktopDisplayEventHandler( Context context, ShellInit shellInit, - Transitions transitions, DisplayController displayController, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - IWindowManager windowManager, Optional<DesktopUserRepositories> desktopUserRepositories, Optional<DesktopTasksController> desktopTasksController, - ShellTaskOrganizer shellTaskOrganizer + Optional<DesktopDisplayModeController> desktopDisplayModeController ) { if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); @@ -1245,13 +1269,10 @@ public abstract class WMShellModule { new DesktopDisplayEventHandler( context, shellInit, - transitions, displayController, - rootTaskDisplayAreaOrganizer, - windowManager, desktopUserRepositories.get(), desktopTasksController.get(), - shellTaskOrganizer)); + desktopDisplayModeController.get())); } @WMSingleton @@ -1381,6 +1402,29 @@ public abstract class WMShellModule { return new DesktopModeUiEventLogger(uiEventLogger, packageManager); } + @WMSingleton + @Provides + static Optional<DesktopDisplayModeController> provideDesktopDisplayModeController( + Context context, + Transitions transitions, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + IWindowManager windowManager, + ShellTaskOrganizer shellTaskOrganizer, + DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider + ) { + if (!DesktopModeStatus.canEnterDesktopMode(context)) { + return Optional.empty(); + } + return Optional.of( + new DesktopDisplayModeController( + context, + transitions, + rootTaskDisplayAreaOrganizer, + windowManager, + shellTaskOrganizer, + desktopWallpaperActivityTokenProvider)); + } + // // App zoom out // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index c38558d7bde9..946e7952dd50 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -16,41 +16,25 @@ package com.android.wm.shell.desktopmode -import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD -import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM -import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED -import android.app.WindowConfiguration.windowingModeToString import android.content.Context -import android.provider.Settings -import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.view.Display.DEFAULT_DISPLAY -import android.view.IWindowManager -import android.view.WindowManager.TRANSIT_CHANGE import android.window.DesktopExperienceFlags -import android.window.WindowContainerTransaction import com.android.internal.protolog.ProtoLog -import com.android.window.flags.Flags -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.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit -import com.android.wm.shell.transition.Transitions /** Handles display events in desktop mode */ class DesktopDisplayEventHandler( private val context: Context, shellInit: ShellInit, - private val transitions: Transitions, private val displayController: DisplayController, - private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, - private val windowManager: IWindowManager, private val desktopUserRepositories: DesktopUserRepositories, private val desktopTasksController: DesktopTasksController, - private val shellTaskOrganizer: ShellTaskOrganizer, + private val desktopDisplayModeController: DesktopDisplayModeController, ) : OnDisplaysChangedListener, OnDeskRemovedListener { private val desktopRepository: DesktopRepository @@ -70,7 +54,7 @@ class DesktopDisplayEventHandler( override fun onDisplayAdded(displayId: Int) { if (displayId != DEFAULT_DISPLAY) { - refreshDisplayWindowingMode() + desktopDisplayModeController.refreshDisplayWindowingMode() } if (!supportsDesks(displayId)) { @@ -88,7 +72,7 @@ class DesktopDisplayEventHandler( override fun onDisplayRemoved(displayId: Int) { if (displayId != DEFAULT_DISPLAY) { - refreshDisplayWindowingMode() + desktopDisplayModeController.refreshDisplayWindowingMode() } // TODO: b/362720497 - move desks in closing display to the remaining desk. @@ -102,65 +86,6 @@ class DesktopDisplayEventHandler( } } - private fun refreshDisplayWindowingMode() { - if (!Flags.enableDisplayWindowingModeSwitching()) return - // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available. - val isExtendedDisplayEnabled = - 0 != - Settings.Global.getInt( - context.contentResolver, - DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, - 0, - ) - if (!isExtendedDisplayEnabled) { - // No action needed in mirror or projected mode. - return - } - - val hasNonDefaultDisplay = - rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId -> - displayId != DEFAULT_DISPLAY - } - val targetDisplayWindowingMode = - if (hasNonDefaultDisplay) { - WINDOWING_MODE_FREEFORM - } else { - // Use the default display windowing mode when no non-default display. - windowManager.getWindowingMode(DEFAULT_DISPLAY) - } - val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) - requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." } - val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode - if (currentDisplayWindowingMode == targetDisplayWindowingMode) { - // Already in the target mode. - return - } - - logV( - "As an external display is connected, changing default display's windowing mode from" + - " ${windowingModeToString(currentDisplayWindowingMode)}" + - " to ${windowingModeToString(targetDisplayWindowingMode)}" - ) - - val wct = WindowContainerTransaction() - wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode) - shellTaskOrganizer - .getRunningTasks(DEFAULT_DISPLAY) - .filter { it.activityType == ACTIVITY_TYPE_STANDARD } - .forEach { - // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy - when (it.windowingMode) { - currentDisplayWindowingMode -> { - wct.setWindowingMode(it.token, currentDisplayWindowingMode) - } - targetDisplayWindowingMode -> { - wct.setWindowingMode(it.token, WINDOWING_MODE_UNDEFINED) - } - } - } - transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) - } - // TODO: b/362720497 - connected/projected display considerations. private fun supportsDesks(displayId: Int): Boolean = DesktopModeStatus.canEnterDesktopMode(context) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt new file mode 100644 index 000000000000..c9a63ff818f5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.app.WindowConfiguration.windowingModeToString +import android.content.Context +import android.provider.Settings +import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS +import android.view.Display.DEFAULT_DISPLAY +import android.view.IWindowManager +import android.view.WindowManager.TRANSIT_CHANGE +import android.window.WindowContainerTransaction +import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.transition.Transitions + +/** Controls the display windowing mode in desktop mode */ +class DesktopDisplayModeController( + private val context: Context, + private val transitions: Transitions, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val windowManager: IWindowManager, + private val shellTaskOrganizer: ShellTaskOrganizer, + private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, +) { + + fun refreshDisplayWindowingMode() { + if (!Flags.enableDisplayWindowingModeSwitching()) return + // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available. + val isExtendedDisplayEnabled = + 0 != + Settings.Global.getInt( + context.contentResolver, + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, + 0, + ) + if (!isExtendedDisplayEnabled) { + // No action needed in mirror or projected mode. + return + } + + val hasNonDefaultDisplay = + rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId -> + displayId != DEFAULT_DISPLAY + } + val targetDisplayWindowingMode = + if (hasNonDefaultDisplay) { + WINDOWING_MODE_FREEFORM + } else { + // Use the default display windowing mode when no non-default display. + windowManager.getWindowingMode(DEFAULT_DISPLAY) + } + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) + requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." } + val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode + if (currentDisplayWindowingMode == targetDisplayWindowingMode) { + // Already in the target mode. + return + } + + logV( + "As an external display is connected, changing default display's windowing mode from" + + " ${windowingModeToString(currentDisplayWindowingMode)}" + + " to ${windowingModeToString(targetDisplayWindowingMode)}" + ) + + val wct = WindowContainerTransaction() + wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode) + shellTaskOrganizer + .getRunningTasks(DEFAULT_DISPLAY) + .filter { it.activityType == ACTIVITY_TYPE_STANDARD } + .forEach { + // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy + when (it.windowingMode) { + currentDisplayWindowingMode -> { + wct.setWindowingMode(it.token, currentDisplayWindowingMode) + } + targetDisplayWindowingMode -> { + wct.setWindowingMode(it.token, WINDOWING_MODE_UNDEFINED) + } + } + } + // The override windowing mode of DesktopWallpaper can be UNDEFINED on fullscreen-display + // right after the first launch while its resolved windowing mode is FULLSCREEN. We here + // it has the FULLSCREEN override windowing mode. + desktopWallpaperActivityTokenProvider.getToken(DEFAULT_DISPLAY)?.let { token -> + wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN) + } + transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) + } + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + companion object { + private const val TAG = "DesktopDisplayModeController" + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index eba1be517147..7c6cf4a8b37f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -208,6 +208,7 @@ class DesktopRepository( /** Adds the given desk under the given display. */ fun addDesk(displayId: Int, deskId: Int) { + logD("addDesk for displayId=%d and deskId=%d", displayId, deskId) desktopData.createDesk(displayId, deskId) } @@ -224,6 +225,7 @@ class DesktopRepository( /** Sets the given desk as the active one in the given display. */ fun setActiveDesk(displayId: Int, deskId: Int) { + logD("setActiveDesk for displayId=%d and deskId=%d", displayId, deskId) desktopData.setActiveDesk(displayId = displayId, deskId = deskId) } @@ -246,6 +248,7 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ fun addTask(displayId: Int, taskId: Int, isVisible: Boolean) { + logD("addTask for displayId=%d, taskId=%d, isVisible=%b", displayId, taskId, isVisible) val activeDesk = checkNotNull(desktopData.getDefaultDesk(displayId)) { "Expected desk in display: $displayId" @@ -254,6 +257,13 @@ class DesktopRepository( } fun addTaskToDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) { + logD( + "addTaskToDesk for displayId=%d, deskId=%d, taskId=%d, isVisible=%b", + displayId, + deskId, + taskId, + isVisible, + ) addOrMoveTaskToTopOfDesk(displayId = displayId, deskId = deskId, taskId = taskId) addActiveTaskToDesk(displayId = displayId, deskId = deskId, taskId = taskId) updateTaskInDesk( @@ -265,6 +275,12 @@ class DesktopRepository( } private fun addActiveTaskToDesk(displayId: Int, deskId: Int, taskId: Int) { + logD( + "addActiveTaskToDesk for displayId=%d, deskId=%d, taskId=%d", + displayId, + deskId, + taskId, + ) val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" } // Removes task if it is active on another desk excluding this desk. @@ -279,6 +295,7 @@ class DesktopRepository( /** Removes task from active task list of desks excluding the [excludedDeskId]. */ @VisibleForTesting fun removeActiveTask(taskId: Int, excludedDeskId: Int? = null) { + logD("removeActiveTask for taskId=%d, excludedDeskId=%d", taskId, excludedDeskId) val affectedDisplays = mutableSetOf<Int>() desktopData .desksSequence() @@ -303,6 +320,7 @@ class DesktopRepository( taskId: Int, notifyListeners: Boolean = true, ): Boolean { + logD("removeActiveTaskFromDesk for deskId=%d, taskId=%d", deskId, taskId) val desk = desktopData.getDesk(deskId) ?: return false if (desk.activeTasks.remove(taskId)) { logD("Removed active task=%d from deskId=%d", taskId, desk.deskId) @@ -314,29 +332,22 @@ class DesktopRepository( return false } - /** - * Adds given task to the closing task list for [displayId]'s active desk. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - fun addClosingTask(displayId: Int, taskId: Int) { - val activeDesk = - desktopData.getActiveDesk(displayId) - ?: error("Expected active desk in display: $displayId") - if (activeDesk.closingTasks.add(taskId)) { - logD( - "Added closing task=%d displayId=%d deskId=%d", - taskId, - displayId, - activeDesk.deskId, - ) + /** Adds given task to the closing task list of its desk. */ + fun addClosingTask(displayId: Int, deskId: Int?, taskId: Int) { + val desk = + deskId?.let { desktopData.getDesk(it) } + ?: checkNotNull(desktopData.getActiveDesk(displayId)) { + "Expected active desk in display: $displayId" + } + if (desk.closingTasks.add(taskId)) { + logD("Added closing task=%d displayId=%d deskId=%d", taskId, displayId, desk.deskId) } else { // If the task hasn't been removed from closing list after it disappeared. logW( "Task with taskId=%d displayId=%d deskId=%d is already closing", taskId, displayId, - activeDesk.deskId, + desk.deskId, ) } } @@ -374,7 +385,8 @@ class DesktopRepository( * Checks if a task is the only visible, non-closing, non-minimized task on the active desk of * the given display, or any display's active desk if [displayId] is [INVALID_DISPLAY]. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - consider forcing callers to use [isOnlyVisibleNonClosingTaskInDesk] with + * an explicit desk id instead of using this function and defaulting to the active one. */ fun isOnlyVisibleNonClosingTask(taskId: Int, displayId: Int = INVALID_DISPLAY): Boolean { val activeDesks = @@ -384,14 +396,27 @@ class DesktopRepository( desktopData.getAllActiveDesks() } return activeDesks.any { desk -> - desk.visibleTasks - .subtract(desk.closingTasks) - .subtract(desk.minimizedTasks) - .singleOrNull() == taskId + isOnlyVisibleNonClosingTaskInDesk( + taskId = taskId, + deskId = desk.deskId, + displayId = desk.displayId, + ) } } /** + * Checks if a task is the only visible, non-closing, non-minimized task on the given desk of + * the given display. + */ + fun isOnlyVisibleNonClosingTaskInDesk(taskId: Int, deskId: Int, displayId: Int): Boolean { + val desk = desktopData.getDesk(deskId) ?: return false + return desk.visibleTasks + .subtract(desk.closingTasks) + .subtract(desk.minimizedTasks) + .singleOrNull() == taskId + } + + /** * Returns the active tasks in the given display's active desk. * * TODO: b/389960283 - migrate callers to [getActiveTaskIdsInDesk]. @@ -456,7 +481,7 @@ class DesktopRepository( /** Removes task from visible tasks of all desks except [excludedDeskId]. */ private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) { - desktopData.forAllDesks { displayId, desk -> + desktopData.forAllDesks { _, desk -> if (desk.deskId != excludedDeskId) { removeVisibleTaskFromDesk(deskId = desk.deskId, taskId = taskId) } @@ -668,6 +693,11 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ fun setTopTransparentFullscreenTaskId(displayId: Int, taskId: Int) { + logD( + "Top transparent fullscreen task set for display: taskId=%d, displayId=%d", + taskId, + displayId, + ) desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId = taskId } @@ -685,6 +715,11 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ fun clearTopTransparentFullscreenTaskId(displayId: Int) { + logD( + "Top transparent fullscreen task cleared for display: taskId=%d, displayId=%d", + desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId, + displayId, + ) desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId = null } @@ -718,6 +753,12 @@ class DesktopRepository( * Unminimizes the task if it is minimized. */ private fun addOrMoveTaskToTopOfDesk(displayId: Int, deskId: Int, taskId: Int) { + logD( + "addOrMoveTaskToTopOfDesk displayId=%d, deskId=%d, taskId=%d", + displayId, + deskId, + taskId, + ) val desk = desktopData.getDesk(deskId) ?: error("Could not find desk: $deskId") logD("addOrMoveTaskToTopOfDesk: display=%d deskId=%d taskId=%d", displayId, deskId, taskId) desktopData.forAllDesks { _, desk1 -> desk1.freeformTasksInZOrder.remove(taskId) } @@ -738,6 +779,7 @@ class DesktopRepository( * desk id instead of using this function and defaulting to the active one. */ fun minimizeTask(displayId: Int, taskId: Int) { + logD("minimizeTask displayId=%d, taskId=%d", displayId, taskId) if (displayId == INVALID_DISPLAY) { // When a task vanishes it doesn't have a displayId. Find the display of the task and // mark it as minimized. @@ -756,7 +798,7 @@ class DesktopRepository( /** Minimizes the task in its desk. */ @VisibleForTesting fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) { - logD("Minimize Task: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId) + logD("MinimizeTaskInDesk: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId) desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId) ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId) updateTaskInDesk(displayId, deskId, taskId, isVisible = false) @@ -771,12 +813,12 @@ class DesktopRepository( * TODO: b/389960283 - consider using [unminimizeTaskFromDesk] instead. */ fun unminimizeTask(displayId: Int, taskId: Int) { - logD("Unminimize Task: display=%d, task=%d", displayId, taskId) + logD("UnminimizeTask: display=%d, task=%d", displayId, taskId) desktopData.forAllDesks(displayId) { desk -> unminimizeTaskFromDesk(desk.deskId, taskId) } } private fun unminimizeTaskFromDesk(deskId: Int, taskId: Int) { - logD("Unminimize Task: deskId=%d, taskId=%d", deskId, taskId) + logD("Unminimize Task from desk: deskId=%d, taskId=%d", deskId, taskId) if (desktopData.getDesk(deskId)?.minimizedTasks?.remove(taskId) != true) { logW("Unminimize Task: deskId=%d, taskId=%d, no task data", deskId, taskId) } @@ -847,6 +889,7 @@ class DesktopRepository( /** Removes the given desk and returns the active tasks in that desk. */ fun removeDesk(deskId: Int): Set<Int> { + logD("removeDesk %d", deskId) val desk = desktopData.getDesk(deskId) ?: return emptySet<Int>().also { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt index e831d5eecdc2..6034299453fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt @@ -19,13 +19,16 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.window.DesktopModeFlags +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.freeform.TaskChangeListener +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE /** Manages tasks handling specific to Android Desktop Mode. */ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUserRepositories) : TaskChangeListener { override fun onTaskOpening(taskInfo: RunningTaskInfo) { + logD("onTaskOpening for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) val desktopRepository: DesktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) if (!isFreeformTask(taskInfo) && desktopRepository.isActiveTask(taskInfo.taskId)) { @@ -38,6 +41,7 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser } override fun onTaskChanging(taskInfo: RunningTaskInfo) { + logD("onTaskChanging for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) val desktopRepository: DesktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return @@ -67,9 +71,15 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser // of race conditions and possible duplications with [onTaskChanging]. override fun onNonTransitionTaskChanging(taskInfo: RunningTaskInfo) { // TODO: b/367268953 - Propagate usages from FreeformTaskListener to this method. + logD( + "onNonTransitionTaskChanging for taskId=%d, displayId=%d", + taskInfo.taskId, + taskInfo.displayId, + ) } override fun onTaskMovingToFront(taskInfo: RunningTaskInfo) { + logD("onTaskMovingToFront for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) val desktopRepository: DesktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return @@ -80,10 +90,12 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser } override fun onTaskMovingToBack(taskInfo: RunningTaskInfo) { + logD("onTaskMovingToBack for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) // TODO: b/367268953 - Connect this with DesktopRepository. } override fun onTaskClosing(taskInfo: RunningTaskInfo) { + logD("onTaskClosing for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) val desktopRepository: DesktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return @@ -104,4 +116,12 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser private fun isFreeformTask(taskInfo: RunningTaskInfo): Boolean = taskInfo.windowingMode == WINDOWING_MODE_FREEFORM + + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + companion object { + private const val TAG = "DesktopTaskChangeListener" + } } 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 93058db0c171..45adfe4112a6 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 @@ -117,6 +117,7 @@ import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.recents.RecentsTransitionStateListener.RecentsTransitionState import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING +import com.android.wm.shell.shared.R as SharedR import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ExternalThread import com.android.wm.shell.shared.annotations.ShellDesktopThread @@ -801,6 +802,9 @@ class DesktopTasksController( ): ((IBinder) -> Unit) { val taskId = taskInfo.taskId val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) + if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + error("Did not find desk for task: $taskId") + } snapEventHandler.removeTaskIfTiled(displayId, taskId) val shouldExitDesktop = willExitDesktop( @@ -818,7 +822,7 @@ class DesktopTasksController( shouldEndUpAtHome = true, ) - taskRepository.addClosingTask(displayId, taskId) + taskRepository.addClosingTask(displayId = displayId, deskId = deskId, taskId = taskId) taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId, taskId) ) @@ -870,6 +874,10 @@ class DesktopTasksController( private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { val taskId = taskInfo.taskId val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) + if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + logW("minimizeTaskInner: desk not found for task: ${taskInfo.taskId}") + return + } val displayId = taskInfo.displayId val wct = WindowContainerTransaction() @@ -890,10 +898,26 @@ class DesktopTasksController( taskInfo = taskInfo, reason = DesktopImmersiveController.ExitReason.MINIMIZED, ) - - wct.reorder(taskInfo.token, false) - val isLastTask = taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId) - val transition: IBinder = + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desksOrganizer.minimizeTask( + wct = wct, + deskId = checkNotNull(deskId) { "Expected non-null deskId" }, + task = taskInfo, + ) + } else { + wct.reorder(taskInfo.token, /* onTop= */ false) + } + val isLastTask = + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + taskRepository.isOnlyVisibleNonClosingTaskInDesk( + taskId = taskId, + deskId = checkNotNull(deskId) { "Expected non-null deskId" }, + displayId = displayId, + ) + } else { + taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId) + } + val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask) desktopTasksLimiter.ifPresent { it.addPendingMinimizeChange( @@ -1231,9 +1255,9 @@ class DesktopTasksController( // home. if (Flags.enablePerDisplayDesktopWallpaperActivity()) { performDesktopExitCleanupIfNeeded( - task.taskId, - task.displayId, - wct, + taskId = task.taskId, + displayId = task.displayId, + wct = wct, forceToFullscreen = false, // TODO: b/371096166 - Temporary turing home relaunch off to prevent home stealing // display focus. Remove shouldEndUpAtHome = false when home focus handling @@ -1800,6 +1824,7 @@ class DesktopTasksController( private fun performDesktopExitCleanupIfNeeded( taskId: Int, + deskId: Int? = null, displayId: Int, wct: WindowContainerTransaction, forceToFullscreen: Boolean, @@ -1813,13 +1838,14 @@ class DesktopTasksController( // |RunOnTransitStart| when the transition is started. return performDesktopExitCleanUp( wct = wct, - deskId = null, + deskId = deskId, displayId = displayId, willExitDesktop = true, shouldEndUpAtHome = shouldEndUpAtHome, ) } + /** TODO: b/394268248 - update [deskId] to be non-null. */ private fun performDesktopExitCleanUp( wct: WindowContainerTransaction, deskId: Int?, @@ -2015,7 +2041,9 @@ class DesktopTasksController( } val cornerRadius = context.resources - .getDimensionPixelSize(R.dimen.desktop_windowing_freeform_rounded_corner_radius) + .getDimensionPixelSize( + SharedR.dimen.desktop_windowing_freeform_rounded_corner_radius + ) .toFloat() info.changes .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM } @@ -2370,17 +2398,28 @@ class DesktopTasksController( ): WindowContainerTransaction? { logV("handleTaskClosing") if (!isDesktopModeShowing(task.displayId)) return null + val deskId = taskRepository.getDeskIdForTask(task.taskId) + if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + return null + } val wct = WindowContainerTransaction() - performDesktopExitCleanupIfNeeded( - task.taskId, - task.displayId, - wct, - forceToFullscreen = false, - ) + val deactivationRunnable = + performDesktopExitCleanupIfNeeded( + taskId = task.taskId, + deskId = deskId, + displayId = task.displayId, + wct = wct, + forceToFullscreen = false, + ) + deactivationRunnable?.invoke(transition) if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { - taskRepository.addClosingTask(task.displayId, task.taskId) + taskRepository.addClosingTask( + displayId = task.displayId, + deskId = deskId, + taskId = task.taskId, + ) snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId) } @@ -2584,9 +2623,9 @@ class DesktopTasksController( wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) performDesktopExitCleanupIfNeeded( - taskInfo.taskId, - taskInfo.displayId, - wct, + taskId = taskInfo.taskId, + displayId = taskInfo.displayId, + wct = wct, forceToFullscreen = false, shouldEndUpAtHome = false, ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl index 59add47fc79d..5f45192569e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl @@ -18,13 +18,11 @@ package com.android.wm.shell.desktopmode; /** * Defines the state of desks on a display whose ID is `displayId`, which is: - * - `canCreateDesks`: whether it's possible to create new desks on this display. * - `activeDeskId`: the currently active desk Id, or `-1` if none is active. * - `deskId`: the list of desk Ids of the available desks on this display. */ parcelable DisplayDeskState { int displayId; - boolean canCreateDesk; int activeDeskId; int[] deskIds; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index b46051c51fcc..cb231800bd63 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -43,7 +43,7 @@ import com.android.wm.shell.bubbles.BubbleTransitions import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP -import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.animation.PhysicsAnimator import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT @@ -120,10 +120,7 @@ sealed class DragToDesktopTransitionHandler( dragToDesktopAnimator: MoveToDesktopAnimator, ) { if (inProgress) { - ProtoLog.v( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DragToDesktop: Drag to desktop transition already in progress.", - ) + logV("Drag to desktop transition already in progress.") return } @@ -537,12 +534,14 @@ sealed class DragToDesktopTransitionHandler( state.cancelState == CancelState.CANCEL_SPLIT_LEFT || state.cancelState == CancelState.CANCEL_SPLIT_RIGHT ) { + logV("mergeAnimation: cancel through split") clearState() return } // In case of bubble animation, finish the initial desktop drag animation, but keep the // current animation running and have bubbles take over if (info.type == TRANSIT_CONVERT_TO_BUBBLE) { + logV("mergeAnimation: convert-to-bubble") state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null) clearState() return @@ -562,6 +561,7 @@ sealed class DragToDesktopTransitionHandler( state.startTransitionFinishCb ?: error("Start transition expected to be waiting for merge but wasn't") if (isEndTransition) { + logV("mergeAnimation: end-transition, target=$mergeTarget") setupEndDragToDesktop( info, startTransaction = startT, @@ -572,7 +572,10 @@ sealed class DragToDesktopTransitionHandler( LatencyTracker.getInstance(context) .onActionEnd(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG) animateEndDragToDesktop(startTransaction = startT, startTransitionFinishCb) - } else if (isCancelTransition) { + return + } + if (isCancelTransition) { + logV("mergeAnimation: cancel-transition, target=$mergeTarget") LatencyTracker.getInstance(context) .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG) info.changes.forEach { change -> @@ -583,7 +586,9 @@ sealed class DragToDesktopTransitionHandler( finishCallback.onTransitionFinished(/* wct= */ null) startTransitionFinishCb.onTransitionFinished(/* wct= */ null) clearState() + return } + logW("unhandled merge transition: transitionInfo=$info") } protected open fun setupEndDragToDesktop( @@ -724,10 +729,7 @@ sealed class DragToDesktopTransitionHandler( return } if (state.startTransitionToken == transition) { - ProtoLog.v( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DragToDesktop: onTransitionConsumed() start transition aborted", - ) + logV("onTransitionConsumed() start transition aborted") state.startAborted = true // The start-transition (DRAG_HOLD) is aborted, cancel its jank interaction. interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) @@ -950,7 +952,16 @@ sealed class DragToDesktopTransitionHandler( CANCEL_BUBBLE_RIGHT, } + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private fun logW(msg: String, vararg arguments: Any?) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + companion object { + private const val TAG = "DragToDesktopTransitionHandler" /** The duration of the animation to commit or cancel the drag-to-desktop gesture. */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L @@ -1052,10 +1063,7 @@ constructor( val state = requireTransitionState() val homeLeash = state.homeChange?.leash if (homeLeash == null) { - ProtoLog.e( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DragToDesktop: home leash is null", - ) + logE("home leash is null") } else { // Hide home on finish to prevent flickering when wallpaper activity flag is enabled finishTransaction.hide(homeLeash) @@ -1096,6 +1104,12 @@ constructor( val startBoundsWithOffset = Rect(startBounds).apply { offset(startPosition.x.toInt(), startPosition.y.toInt()) } + logV( + "animateEndDragToDesktop: startBounds=$startBounds, endBounds=$endBounds, " + + "startScale=$startScale, startPosition=$startPosition, " + + "startBoundsWithOffset=$startBoundsWithOffset" + ) + dragToDesktopStateListener?.onCommitToDesktopAnimationStart() // Accept the merge by applying the merging transaction (applied by #showResizeVeil) // and finish callback. Show the veil and position the task at the first frame before @@ -1177,7 +1191,16 @@ constructor( .start() } + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private fun logE(msg: String, vararg arguments: Any?) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + companion object { + private const val TAG = "SpringDragToDesktopTransitionHandler" /** The freeform tasks initial scale when committing the drag-to-desktop gesture. */ private val FREEFORM_TASKS_INITIAL_SCALE = propertyValue("freeform_tasks_initial_scale", scale = 100f, default = 0.9f) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl index 7ed1581cdfdb..cefbd8947feb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl @@ -28,7 +28,7 @@ oneway interface IDesktopTaskListener { * Called once when the listener first gets connected to initialize it with the current state of * desks in Shell. */ - void onListenerConnected(in DisplayDeskState[] displayDeskStates); + void onListenerConnected(in DisplayDeskState[] displayDeskStates, boolean canCreateDesks); /** Desktop tasks visibility has changed. Visible if at least 1 task is visible. */ void onTasksVisibilityChanged(int displayId, int visibleTasksCount); @@ -49,10 +49,10 @@ oneway interface IDesktopTaskListener { void onExitDesktopModeTransitionStarted(int transitionDuration); /** - * Called when the conditions that allow the creation of a new desk on the display whose ID is - * `displayId` changes to `canCreateDesks`. It's also called when a new display is added. + * Called when the conditions that allow the creation of a new desk changes. This is a global + * state for the entire device. */ - void onCanCreateDesksChanged(int displayId, boolean canCreateDesks); + void onCanCreateDesksChanged(boolean canCreateDesks); /** Called when a desk whose ID is `deskId` is added to the display whose ID is `displayId`. */ void onDeskAdded(int displayId, int deskId); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index 0f2f3711a9a3..fc359d7d67b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -40,9 +40,19 @@ interface DesksOrganizer { task: ActivityManager.RunningTaskInfo, ) + /** Minimizes the given task of the given deskId. */ + fun minimizeTask( + wct: WindowContainerTransaction, + deskId: Int, + task: ActivityManager.RunningTaskInfo, + ) + /** Whether the change is for the given desk id. */ fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean + /** Whether the change is for a known desk. */ + fun isDeskChange(change: TransitionInfo.Change): Boolean + /** * Returns the desk id in which the task in the given change is located at the end of a * transition, if any. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt index 339932cabd2c..f576258ebdaa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -15,7 +15,9 @@ */ package com.android.wm.shell.desktopmode.multidesks +import android.annotation.SuppressLint import android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityTaskManager.INVALID_TASK_ID import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -25,6 +27,7 @@ import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.DesktopExperienceFlags import android.window.TransitionInfo +import android.window.WindowContainerToken import android.window.WindowContainerTransaction import androidx.core.util.forEach import com.android.internal.annotations.VisibleForTesting @@ -43,8 +46,12 @@ class RootTaskDesksOrganizer( private val shellTaskOrganizer: ShellTaskOrganizer, ) : DesksOrganizer, ShellTaskOrganizer.TaskListener { - private val deskCreateRequests = mutableListOf<CreateRequest>() - @VisibleForTesting val roots = SparseArray<DeskRoot>() + private val createDeskRootRequests = mutableListOf<CreateDeskRequest>() + @VisibleForTesting val deskRootsByDeskId = SparseArray<DeskRoot>() + private val createDeskMinimizationRootRequests = + mutableListOf<CreateDeskMinimizationRootRequest>() + @VisibleForTesting + val deskMinimizationRootsByDeskId: MutableMap<Int, DeskMinimizationRoot> = mutableMapOf() init { if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { @@ -57,7 +64,7 @@ class RootTaskDesksOrganizer( override fun createDesk(displayId: Int, callback: OnCreateCallback) { logV("createDesk in display: %d", displayId) - deskCreateRequests += CreateRequest(displayId, callback) + createDeskRootRequests += CreateDeskRequest(displayId, callback) shellTaskOrganizer.createRootTask( displayId, WINDOWING_MODE_FREEFORM, @@ -68,14 +75,14 @@ class RootTaskDesksOrganizer( override fun removeDesk(wct: WindowContainerTransaction, deskId: Int) { logV("removeDesk %d", deskId) - val desk = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" } - wct.removeRootTask(desk.taskInfo.token) + deskRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) } + deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) } } override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) { logV("activateDesk %d", deskId) - val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" } - wct.reorder(root.taskInfo.token, /* onTop= */ true) + val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } + wct.reorder(root.token, /* onTop= */ true) wct.setLaunchRoot( /* container= */ root.taskInfo.token, /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED), @@ -85,7 +92,7 @@ class RootTaskDesksOrganizer( override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) { logV("deactivateDesk %d", deskId) - val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" } + val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } wct.setLaunchRoot( /* container= */ root.taskInfo.token, /* windowingModes= */ null, @@ -98,16 +105,58 @@ class RootTaskDesksOrganizer( deskId: Int, task: RunningTaskInfo, ) { - val root = roots[deskId] ?: error("Root not found for desk: $deskId") + val root = deskRootsByDeskId[deskId] ?: error("Root not found for desk: $deskId") wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED) wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true) } + override fun minimizeTask(wct: WindowContainerTransaction, deskId: Int, task: RunningTaskInfo) { + val deskRoot = + checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } + val minimizationRoot = + checkNotNull(deskMinimizationRootsByDeskId[deskId]) { + "Minimization root not found for desk: $deskId" + } + val taskId = task.taskId + if (taskId in minimizationRoot.children) { + logV("Task #$taskId is already minimized in desk #$deskId") + return + } + if (taskId !in deskRoot.children) { + logE("Attempted to minimize task=${task.taskId} in desk=$deskId but it was not a child") + return + } + wct.reparent(task.token, minimizationRoot.token, /* onTop= */ true) + } + override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean = - roots.contains(deskId) && change.taskInfo?.taskId == deskId + (isDeskRootChange(change) && change.taskId == deskId) || + (getDeskMinimizationRootInChange(change)?.deskId == deskId) + + override fun isDeskChange(change: TransitionInfo.Change): Boolean = + isDeskRootChange(change) || getDeskMinimizationRootInChange(change) != null + + private fun isDeskRootChange(change: TransitionInfo.Change): Boolean = + change.taskId in deskRootsByDeskId - override fun getDeskAtEnd(change: TransitionInfo.Change): Int? = - change.taskInfo?.parentTaskId?.takeIf { it in roots } + private fun getDeskMinimizationRootInChange( + change: TransitionInfo.Change + ): DeskMinimizationRoot? = + deskMinimizationRootsByDeskId.values.find { it.rootId == change.taskId } + + private val TransitionInfo.Change.taskId: Int + get() = taskInfo?.taskId ?: INVALID_TASK_ID + + override fun getDeskAtEnd(change: TransitionInfo.Change): Int? { + val parentTaskId = change.taskInfo?.parentTaskId ?: return null + if (parentTaskId in deskRootsByDeskId) { + return parentTaskId + } + val deskMinimizationRoot = + deskMinimizationRootsByDeskId.values.find { root -> root.rootId == parentTaskId } + ?: return null + return deskMinimizationRoot.deskId + } override fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean = change.taskInfo?.taskId == deskId && @@ -115,51 +164,176 @@ class RootTaskDesksOrganizer( change.mode == TRANSIT_TO_FRONT override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { - if (taskInfo.parentTaskId in roots) { + // Check whether this task is appearing inside a desk. + if (taskInfo.parentTaskId in deskRootsByDeskId) { val deskId = taskInfo.parentTaskId val taskId = taskInfo.taskId logV("Task #$taskId appeared in desk #$deskId") addChildToDesk(taskId = taskId, deskId = deskId) return } - val deskId = taskInfo.taskId - check(deskId !in roots) { "A root already exists for desk: $deskId" } - val request = - checkNotNull(deskCreateRequests.firstOrNull { it.displayId == taskInfo.displayId }) { - "Task ${taskInfo.taskId} appeared without pending create request" - } - logV("Desk #$deskId appeared") - roots[deskId] = DeskRoot(deskId, taskInfo, leash) - deskCreateRequests.remove(request) - request.onCreateCallback.onCreated(deskId) + // Check whether this task is appearing in a minimization root. + val minimizationRoot = + deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == taskInfo.parentTaskId } + if (minimizationRoot != null) { + val deskId = minimizationRoot.deskId + val taskId = taskInfo.taskId + logV("Task #$taskId was minimized in desk #$deskId ") + addChildToMinimizationRoot(taskId = taskId, deskId = deskId) + return + } + // The appearing task is a root (either a desk or a minimization root), it should not exist + // already. + check(taskInfo.taskId !in deskRootsByDeskId) { + "A root already exists for desk: ${taskInfo.taskId}" + } + check(deskMinimizationRootsByDeskId.values.none { it.rootId == taskInfo.taskId }) { + "A minimization root already exists with rootId: ${taskInfo.taskId}" + } + + val appearingInDisplayId = taskInfo.displayId + // Check if there's any pending desk creation requests under this display. + val deskRequest = + createDeskRootRequests.firstOrNull { it.displayId == appearingInDisplayId } + if (deskRequest != null) { + // Appearing root matches desk request. + val deskId = taskInfo.taskId + logV("Desk #$deskId appeared") + deskRootsByDeskId[deskId] = DeskRoot(deskId, taskInfo, leash) + createDeskRootRequests.remove(deskRequest) + deskRequest.onCreateCallback.onCreated(deskId) + createDeskMinimizationRoot(displayId = appearingInDisplayId, deskId = deskId) + return + } + // Check if there's any pending minimization container creation requests under this display. + val deskMinimizationRootRequest = + createDeskMinimizationRootRequests.first { it.displayId == appearingInDisplayId } + val deskId = deskMinimizationRootRequest.deskId + logV("Minimization container for desk #$deskId appeared with id=${taskInfo.taskId}") + val deskMinimizationRoot = DeskMinimizationRoot(deskId, taskInfo, leash) + deskMinimizationRootsByDeskId[deskId] = deskMinimizationRoot + createDeskMinimizationRootRequests.remove(deskMinimizationRootRequest) + hideMinimizationRoot(deskMinimizationRoot) } override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { - if (roots.contains(taskInfo.taskId)) { + if (deskRootsByDeskId.contains(taskInfo.taskId)) { val deskId = taskInfo.taskId - roots[deskId] = roots[deskId].copy(taskInfo = taskInfo) + deskRootsByDeskId[deskId] = deskRootsByDeskId[deskId].copy(taskInfo = taskInfo) + logV("Desk #$deskId's task info changed") + return } + val minimizationRoot = + deskMinimizationRootsByDeskId.values.find { root -> root.rootId == taskInfo.taskId } + if (minimizationRoot != null) { + deskMinimizationRootsByDeskId.remove(minimizationRoot.deskId) + deskMinimizationRootsByDeskId[minimizationRoot.deskId] = + minimizationRoot.copy(taskInfo = taskInfo) + logV("Minimization root for desk#${minimizationRoot.deskId} task info changed") + return + } + + val parentTaskId = taskInfo.parentTaskId + if (parentTaskId in deskRootsByDeskId) { + val deskId = taskInfo.parentTaskId + val taskId = taskInfo.taskId + logV("onTaskInfoChanged: Task #$taskId appeared in desk #$deskId") + addChildToDesk(taskId = taskId, deskId = deskId) + return + } + // Check whether this task is appearing in a minimization root. + val parentMinimizationRoot = + deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == parentTaskId } + if (parentMinimizationRoot != null) { + val deskId = parentMinimizationRoot.deskId + val taskId = taskInfo.taskId + logV("onTaskInfoChanged: Task #$taskId was minimized in desk #$deskId ") + addChildToMinimizationRoot(taskId = taskId, deskId = deskId) + return + } + logE("onTaskInfoChanged: unknown task: ${taskInfo.taskId}") } override fun onTaskVanished(taskInfo: RunningTaskInfo) { - if (roots.contains(taskInfo.taskId)) { + if (deskRootsByDeskId.contains(taskInfo.taskId)) { val deskId = taskInfo.taskId - val deskRoot = roots[deskId] + val deskRoot = deskRootsByDeskId[deskId] // Use the last saved taskInfo to obtain the displayId. Using the local one here will // return -1 since the task is not unassociated with a display. val displayId = deskRoot.taskInfo.displayId logV("Desk #$deskId vanished from display #$displayId") - roots.remove(deskId) + deskRootsByDeskId.remove(deskId) + return + } + val deskMinimizationRoot = + deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == taskInfo.taskId } + if (deskMinimizationRoot != null) { + logV("Minimization root for desk ${deskMinimizationRoot.deskId} vanished") + deskMinimizationRootsByDeskId.remove(deskMinimizationRoot.deskId) return } + + // Check whether the vanishing task was a child of any desk. // At this point, [parentTaskId] may be unset even if this is a task vanishing from a desk, // so search through each root to remove this if it's a child. - roots.forEach { deskId, deskRoot -> + deskRootsByDeskId.forEach { deskId, deskRoot -> if (deskRoot.children.remove(taskInfo.taskId)) { logV("Task #${taskInfo.taskId} vanished from desk #$deskId") return } } + // Check whether the vanishing task was a child of the minimized root and remove it. + deskMinimizationRootsByDeskId.values.forEach { root -> + val taskId = taskInfo.taskId + if (root.children.remove(taskId)) { + logV("Task #$taskId vanished from minimization root of desk #${root.deskId}") + return + } + } + } + + private fun createDeskMinimizationRoot(displayId: Int, deskId: Int) { + createDeskMinimizationRootRequests += + CreateDeskMinimizationRootRequest(displayId = displayId, deskId = deskId) + shellTaskOrganizer.createRootTask( + displayId, + WINDOWING_MODE_FREEFORM, + /* listener = */ this, + /* removeWithTaskOrganizer = */ true, + ) + } + + @SuppressLint("MissingPermission") + private fun hideMinimizationRoot(root: DeskMinimizationRoot) { + shellTaskOrganizer.applyTransaction( + WindowContainerTransaction().apply { setHidden(root.token, /* hidden= */ true) } + ) + } + + private fun addChildToDesk(taskId: Int, deskId: Int) { + deskRootsByDeskId.forEach { _, deskRoot -> + if (deskRoot.deskId == deskId) { + deskRoot.children.add(taskId) + } else { + deskRoot.children.remove(taskId) + } + } + // A task cannot be in both a desk root and a minimization root at the same time, so make + // sure to remove them if needed. + deskMinimizationRootsByDeskId.values.forEach { root -> root.children.remove(taskId) } + } + + private fun addChildToMinimizationRoot(taskId: Int, deskId: Int) { + deskMinimizationRootsByDeskId.forEach { _, minimizationRoot -> + if (minimizationRoot.deskId == deskId) { + minimizationRoot.children += taskId + } else { + minimizationRoot.children -= taskId + } + } + // A task cannot be in both a desk root and a minimization root at the same time, so make + // sure to remove them if needed. + deskRootsByDeskId.forEach { _, deskRoot -> deskRoot.children -= taskId } } @VisibleForTesting @@ -168,34 +342,55 @@ class RootTaskDesksOrganizer( val taskInfo: RunningTaskInfo, val leash: SurfaceControl, val children: MutableSet<Int> = mutableSetOf(), + ) { + val token: WindowContainerToken = taskInfo.token + } + + @VisibleForTesting + data class DeskMinimizationRoot( + val deskId: Int, + val taskInfo: RunningTaskInfo, + val leash: SurfaceControl, + val children: MutableSet<Int> = mutableSetOf(), + ) { + val rootId: Int + get() = taskInfo.taskId + + val token: WindowContainerToken = taskInfo.token + } + + private data class CreateDeskRequest( + val displayId: Int, + val onCreateCallback: OnCreateCallback, ) + private data class CreateDeskMinimizationRootRequest(val displayId: Int, val deskId: Int) + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private fun logE(msg: String, vararg arguments: Any?) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + override fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("$prefix$TAG") pw.println("${innerPrefix}Desk Roots:") - roots.forEach { deskId, root -> + deskRootsByDeskId.forEach { deskId, root -> + val minimizationRoot = deskMinimizationRootsByDeskId[deskId] pw.println("$innerPrefix #$deskId visible=${root.taskInfo.isVisible}") + pw.println("$innerPrefix displayId=${root.taskInfo.displayId}") pw.println("$innerPrefix children=${root.children}") - } - } - - private fun addChildToDesk(taskId: Int, deskId: Int) { - roots.forEach { _, deskRoot -> - if (deskRoot.deskId == deskId) { - deskRoot.children.add(taskId) - } else { - deskRoot.children.remove(taskId) + pw.println("$innerPrefix minimization root:") + pw.println("$innerPrefix rootId=${minimizationRoot?.rootId}") + if (minimizationRoot != null) { + pw.println("$innerPrefix children=${minimizationRoot.children}") } } } - private data class CreateRequest(val displayId: Int, val onCreateCallback: OnCreateCallback) - - private fun logV(msg: String, vararg arguments: Any?) { - ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) - } - companion object { private const val TAG = "RootTaskDesksOrganizer" } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md index 3fad28ad232f..a98ae5566394 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md @@ -9,6 +9,7 @@ particular order): 4) [Threading model in the Shell](threading.md) 5) [Making changes in the Shell](changes.md) 6) [Extending the Shell for Products/OEMs](extending.md) +6) [Shell transitions](transitions.md) 7) [Debugging in the Shell](debugging.md) 8) [Testing in the Shell](testing.md) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/transitions.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/transitions.md new file mode 100644 index 000000000000..dc23bb0c77d7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/transitions.md @@ -0,0 +1,118 @@ +# Shell transitions +[Back to home](README.md) + +--- + +## General + +General guides for using Shell Transitions can be found here: +- [Shell transitions animation guide](http://go/shell-transit-anim) +- [Hitchhiker's guide to transitions](http://go/transitions-book) + +## Transient-launch transitions +<span style="color:orange">Use with care!</span> + +Transient-launch transitions are a way to handle non-atomic (ie. gestural) transitions by allowing +WM Core to put participating activities into a transiently visible or hidden state for the duration +of the animation and adding the ability to cancel the transition. + +For example, if you are launching an activity normally, WM Core will be updated +at the start of the animation which includes pausing the previous activity and resuming the next +activity (and subsequently the transition will reconcile that state via an animation). + +If you are transiently launching an activity though, WM Core will ensure that both the leaving +activity and the incoming activity will be RESUMED for the duration of the transition duration. In +addition, WM Core will track the position of the transient-launch activity in the window hierarchy +prior to the launch, and allow Shell to restore it to that position if the transitions needs to be +canceled. + +Starting a transient-launch transition can be done via the activity options (since the activity may +not have been started yet): +```kotlin +val opts = ActivityOptions.makeBasic().setTransientLaunch() +val wct = WindowContainerTransaction() +wct.sendPendingIntent(pendingIntent, new Intent(), opts.toBundle()) +transitions.startTransition(TRANSIT_OPEN, wct, ...) +``` + +And restoring the transient order via a WCT: +```kotlin +val wct = WindowContainerTransaction() +wct.restoreTransientOrder(transientLaunchContainerToken) +transitions.startTransition(TRANSIT_RESTORE, wct, ...) +``` + +### <span style="color:orange">Considerations</span> + +Usage of transient-launch transitions should be done with consideration, there are a few gotchas +that might result in subtle and hard-to-reproduce issues. + +#### Understanding the flow +When starting a transient-launch transition, there are several possible outcomes: +1) The transition finishes as normal: The user is committing the transition to the state requested + at the start of the transition. In such cases, you can simply finish the transition and the + states of the transiently shown/hidden activities will be updated to match the original state + that a non-transient transition would have (ie. closing activities will be stopped). + +2) The transition is interrupted: A change in the system results in the window hierarchy changing + in a way which may or may not affect the transient-launch activity. eg. We transiently-launch + home from app A, but then app B launches. In this case, WM attempts to create a new transition + reflecting the window hierarchy changes (ie. if B occludes Home in the above example, then the + transition will have Home TO_BACK, and B TO_FRONT). + + At this point, the transition handler can choose to merge the incoming transition or not (to + queue it after this current transition). Take note of the next section for concerns re. bookend + transitions. + +3) The transition is canceled: The user is canceling the transition to the previous state. In such + cases, you need to store the `WindowContainerToken` for the task associated with the + transient-launch activity, and restore the transient order via the `WindowContainerTransaction` + API above. In some cases, if anything has been reordered since (ie. due to other merged + transitions), then you may also want to use `WindowContainerTransaction#reorder()` to place all + the relevant containers to their original order (provided via the change-order in the initial + launch transition). + +#### Finishing the transient-launch transition + +When restoring the transient order in the 3rd flow above, it is recommended to do it in a new +transition and <span style="color:orange">**not**</span> via the WindowContainerTransaction in +`TransitionFinishCallback#onTransitionFinished()` provided when starting the transition. + +Changes to the window hierarchy via the finish transaction are not applied in sync with other +transitions that are collecting and aplying, and are also not observable in Shell in any way. +Starting a new transition instead ensures both. (The finish transaction can still be used if there +are non-transition affecting properties (ie. container properties) that need to be updated as a part +of finishing the transient-launch transition). + +So the general idea is when restoring is: + +1) Start transient-launch transition START_T +2) ... +3) All done, start bookend transition END_T +4) Handler receives END_T, merges it and then finishes START_T + +In practice it's not quite that simple, due to the ordering of transitions and extra care must be +taken when using a new transition to prevent deadlocking when merging transitions. + +When a new transition arrives while a transient-launch transition is playing, the handler can +choose to handle/merge the transition into the ongoing one, or skip merging to queue it up to be +played after. In the above flow, we can see how this might result in a deadlock: + +Queueing END during merge: +1) Start transient-launch transition START_T +2) ... +3) Incoming transition OTHER_T, choose to cancel START_T -> start bookend transition END_T, but don't merge OTHER_T +3) Waiting for END_T... <span style="color:red">Deadlock!</span> + +Interrupt while pending END: +1) Start transient-launch transition START_T +2) ... +3) All done, start bookend transition END_T +3) Incoming transition OTHER_T occurs before END_T, but don't merge OTHER_T +3) Waiting for END_T... <span style="color:red">Deadlock!</span> + +This means that when using transient-launch transitions with a bookend transition +<span style="color:orange">requires</span> you to handle any incoming transitions if the bookend is +ever queued (or already posted) after it. You can do so by preempting the bookend transition +(finishing the transient-launch transition), or handling the merge of the new transition (so it +doesn't queue).
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java index 118ad9c4bfe3..fa14bc91c8ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java @@ -30,6 +30,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.view.accessibility.IWindowSurfaceInfoCallback; import android.window.ScreenCapture; import androidx.annotation.BinderThread; @@ -373,6 +374,13 @@ public class PipAccessibilityInteractionConnection { } @Override + public void getWindowSurfaceInfo(IWindowSurfaceInfoCallback callback) { + // AbstractAccessibilityServiceConnection uses the standard + // IAccessibilityInteractionConnection for takeScreenshotOfWindow for Pip windows, + // so do nothing here. + } + + @Override public void clearAccessibilityFocus() throws RemoteException { // Do nothing } 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 99c9302edb75..1ce24f76ada5 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 @@ -404,6 +404,10 @@ public class PipController implements ConfigurationChangeListener, mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, pictureInPictureParams, mPipBoundsAlgorithm); + + // Update the size spec in case aspect ratio is invariant, but display has changed + // since the last PiP session, or this is the first PiP session altogether. + mPipBoundsState.updateMinMaxSize(mPipBoundsState.getAspectRatio()); return mPipBoundsAlgorithm.getEntryDestinationBounds(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 035c93db7ee4..97b3e5a2da87 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -738,6 +738,13 @@ public class PipTransition extends PipTransitionController implements } } + if (!mPipTransitionState.isInSwipePipToHomeTransition()) { + // Update the size spec in case aspect ratio is invariant, but display has changed + // since the last PiP session, or this is the first PiP session altogether. + // Skip the update if in swipe PiP to home, as this has already been done. + mPipBoundsState.updateMinMaxSize(mPipBoundsState.getAspectRatio()); + } + // calculate the entry bounds and notify core to move task to pinned with final bounds final Rect entryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); mPipBoundsState.setBounds(entryBounds); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 847a0383e7d0..3e03e001c49b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -47,6 +47,7 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IApplicationThread; import android.app.PendingIntent; +import android.app.WindowConfiguration; import android.content.Context; import android.content.Intent; import android.graphics.Color; @@ -75,11 +76,11 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.Flags; -import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.R; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.HomeTransitionObserver; @@ -320,7 +321,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "RecentsTransitionHandler.mergeAnimation: no controller found"); return; } - controller.merge(info, startT, finishT, mergeTarget, finishCallback); + controller.merge(info, startT, finishT, finishCallback); } @Override @@ -408,7 +409,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // next called. private Pair<int[], TaskSnapshot[]> mPendingPauseSnapshotsForCancel; - // Used to track a pending finish transition + // Used to track a pending finish transition, this is only non-null if + // enableRecentsBookendTransition() is enabled private IBinder mPendingFinishTransition; private IResultReceiver mPendingRunnerFinishCb; @@ -917,7 +919,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, */ @SuppressLint("NewApi") void merge(TransitionInfo info, SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT, IBinder mergeTarget, + SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCallback) { if (mFinishCB == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -927,16 +929,25 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, return; } - if (Flags.enableRecentsBookendTransition() - && info.getType() == TRANSIT_END_RECENTS_TRANSITION - && mergeTarget == mTransition) { - // This is a pending finish, so merge the end transition to trigger completing the - // cleanup of the recents transition - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, - "[%d] RecentsController.merge: TRANSIT_END_RECENTS_TRANSITION", - mInstanceId); - finishCallback.onTransitionFinished(null /* wct */); - return; + if (Flags.enableRecentsBookendTransition()) { + if (info.getType() == TRANSIT_END_RECENTS_TRANSITION) { + // This is a pending finish, so merge the end transition to trigger completing + // the cleanup of the recents transition + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.merge: TRANSIT_END_RECENTS_TRANSITION", + mInstanceId); + consumeMerge(info, startT, finishT, finishCallback); + return; + } else if (mPendingFinishTransition != null) { + // This transition is interrupting a pending finish that was already sent, so + // pre-empt the pending finish transition since the state has already changed + // in the core + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.merge: Awaiting TRANSIT_END_RECENTS_TRANSITION", + mInstanceId); + onFinishInner(null /* wct */); + return; + } } if (info.getType() == TRANSIT_SLEEP) { @@ -1210,16 +1221,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } return; } + // At this point, we are accepting the merge. - startT.apply(); - // Since we're accepting the merge, update the finish transaction so that changes via - // that transaction will be applied on top of those of the merged transitions - mFinishTransaction = finishT; + consumeMerge(info, startT, finishT, finishCallback); + + // Notify Launcher of the new opening tasks if necessary boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(); - if (!passTransitionInfo) { - // not using the incoming anim-only surfaces - info.releaseAnimSurfaces(); - } if (appearedTargets != null) { try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -1229,6 +1236,27 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, Slog.e(TAG, "Error sending appeared tasks to recents animation", e); } } + } + + /** + * Consumes the merge of the other given transition. + */ + private void consumeMerge(TransitionInfo info, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, + Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.merge: consuming merge", + mInstanceId); + + startT.apply(); + // Since we're accepting the merge, update the finish transaction so that changes via + // that transaction will be applied on top of those of the merged transitions + mFinishTransaction = finishT; + boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(); + if (!passTransitionInfo) { + // not using the incoming anim-only surfaces + info.releaseAnimSurfaces(); + } finishCallback.onTransitionFinished(null /* wct */); } @@ -1346,9 +1374,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, final SurfaceControl.Transaction t = mFinishTransaction; final WindowContainerTransaction wct = new WindowContainerTransaction(); + // The following code must set this if it is changing anything in core that might affect + // transitions as a part of finishing the recents transition + boolean requiresBookendTransition = false; + if (mKeyguardLocked && mRecentsTask != null) { if (toHome) wct.reorder(mRecentsTask, true /* toTop */); else wct.restoreTransientOrder(mRecentsTask); + // We are manipulating the window hierarchy, which should only be done with the + // bookend transition + requiresBookendTransition = true; } if (returningToApp) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " returning to app"); @@ -1365,6 +1400,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } + // We are manipulating the window hierarchy, which should only be done with the + // bookend transition + requiresBookendTransition = true; } else if (toHome && mOpeningSeparateHome && mPausingTasks != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " 3p launching home"); // Special situation where 3p launcher was changed during recents (this happens @@ -1384,6 +1422,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } + // We are manipulating the window hierarchy, which should only be done with the + // bookend transition + requiresBookendTransition = true; } else { if (mPausingSeparateHome) { if (mOpeningTasks.isEmpty()) { @@ -1484,13 +1525,21 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, if (Flags.enableRecentsBookendTransition()) { if (!wct.isEmpty()) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, - "[%d] RecentsController.finishInner: " - + "Queuing TRANSIT_END_RECENTS_TRANSITION", mInstanceId); - mPendingRunnerFinishCb = runnerFinishCb; - mPendingFinishTransition = mTransitions.startTransition( - TRANSIT_END_RECENTS_TRANSITION, wct, - new PendingFinishTransitionHandler()); + if (requiresBookendTransition) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.finishInner: " + + "Queuing TRANSIT_END_RECENTS_TRANSITION", mInstanceId); + mPendingRunnerFinishCb = runnerFinishCb; + mPendingFinishTransition = mTransitions.startTransition( + TRANSIT_END_RECENTS_TRANSITION, wct, + new PendingFinishTransitionHandler()); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.finishInner: Non-transition affecting wct", + mInstanceId); + mPendingRunnerFinishCb = runnerFinishCb; + onFinishInner(wct); + } } else { // If there's no work to do, just go ahead and clean up mPendingRunnerFinishCb = runnerFinishCb; @@ -1631,6 +1680,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] PendingFinishTransitionHandler.startAnimation: " + + "Started pending finish transition", mInstanceId); return false; } @@ -1644,10 +1696,15 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, @Override public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishTransaction) { + if (mPendingFinishTransition == null) { + // The cleanup was pre-empted by an earlier transition, nothing there is nothing + // to do here + return; + } // Once we have merged (or not if the WCT didn't result in any changes), then we can // run the pending finish logic ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, - "[%d] RecentsController.onTransitionConsumed: " + "[%d] PendingFinishTransitionHandler.onTransitionConsumed: " + "Consumed pending finish transition", mInstanceId); onFinishInner(null /* wct */); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 73b42d6f007c..77a7c5406a67 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -1317,6 +1317,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, WindowContainerTransaction noFocus = new WindowContainerTransaction(); noFocus.setFocusable(mRootTaskInfo.token, false); mSyncQueue.queue(noFocus); + // Remove touch layers, since offscreen apps coming onscreen will not need their touch + // layers anymore. populateTouchZones() is called in the end callback to inflate new touch + // layers in the appropriate places. + mSplitLayout.removeTouchZones(); mSplitLayout.playSwapAnimation(t, topLeftStage, bottomRightStage, insets -> { @@ -1337,6 +1341,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSyncQueue.runInSync(st -> { mSplitLayout.updateStateWithCurrentPosition(); updateSurfaceBounds(mSplitLayout, st, false /* applyResizingOffset */); + mSplitLayout.populateTouchZones(); // updateSurfaceBounds(), above, officially puts the two apps in their new // stages. Starting on the next frame, all calculations are made using the diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java index 1917996d48fb..938885cc1684 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java @@ -21,6 +21,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE; import static com.android.wm.shell.transition.Transitions.TransitionObserver; import android.annotation.NonNull; @@ -35,6 +36,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.shared.IHomeTransitionListener; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; /** * The {@link TransitionObserver} that observes for transitions involving the home @@ -48,6 +50,8 @@ public class HomeTransitionObserver implements TransitionObserver, private @NonNull final Context mContext; private @NonNull final ShellExecutor mMainExecutor; + private Boolean mPendingHomeVisibilityUpdate; + public HomeTransitionObserver(@NonNull Context context, @NonNull ShellExecutor mainExecutor) { mContext = context; @@ -59,28 +63,78 @@ public class HomeTransitionObserver implements TransitionObserver, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction) { + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + handleTransitionReadyWithBubbleAnything(info); + } else { + handleTransitionReady(info); + } + } + + private void handleTransitionReady(@NonNull TransitionInfo info) { for (TransitionInfo.Change change : info.getChanges()) { final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (info.getType() == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP - || taskInfo == null + if (taskInfo == null + || info.getType() == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP || taskInfo.displayId != DEFAULT_DISPLAY || taskInfo.taskId == -1 || !taskInfo.isRunning) { continue; } + Boolean homeVisibilityUpdate = getHomeVisibilityUpdate(info, change, taskInfo); + if (homeVisibilityUpdate != null) { + notifyHomeVisibilityChanged(homeVisibilityUpdate); + } + } + } + + private void handleTransitionReadyWithBubbleAnything(@NonNull TransitionInfo info) { + Boolean homeVisibilityUpdate = null; + for (TransitionInfo.Change change : info.getChanges()) { + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo == null + || taskInfo.displayId != DEFAULT_DISPLAY + || taskInfo.taskId == -1 + || !taskInfo.isRunning) { + continue; + } + + Boolean update = getHomeVisibilityUpdate(info, change, taskInfo); + if (update != null) { + homeVisibilityUpdate = update; + } + } + + if (info.getType() == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP) { + // Do not apply at the start of desktop drag as that updates launcher UI visibility. + // Store the value and apply with a next transition if needed. + mPendingHomeVisibilityUpdate = homeVisibilityUpdate; + return; + } + + if (info.getType() == TRANSIT_CONVERT_TO_BUBBLE && homeVisibilityUpdate == null) { + // We are converting to bubble and we did not get a change to home visibility in this + // transition. Apply the value from start of drag. + homeVisibilityUpdate = mPendingHomeVisibilityUpdate; + } + if (homeVisibilityUpdate != null) { + mPendingHomeVisibilityUpdate = null; + notifyHomeVisibilityChanged(homeVisibilityUpdate); + } + } - final int mode = change.getMode(); - final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED); - if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) { - final boolean gestureToHomeTransition = isBackGesture - && TransitionUtil.isClosingType(info.getType()); - if (gestureToHomeTransition || TransitionUtil.isClosingMode(mode) - || (!isBackGesture && TransitionUtil.isOpeningMode(mode))) { - notifyHomeVisibilityChanged(gestureToHomeTransition - || TransitionUtil.isOpeningType(mode)); - } + private Boolean getHomeVisibilityUpdate(TransitionInfo info, + TransitionInfo.Change change, ActivityManager.RunningTaskInfo taskInfo) { + final int mode = change.getMode(); + final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED); + if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) { + final boolean gestureToHomeTransition = isBackGesture + && TransitionUtil.isClosingType(info.getType()); + if (gestureToHomeTransition || TransitionUtil.isClosingMode(mode) + || (!isBackGesture && TransitionUtil.isOpeningMode(mode))) { + return gestureToHomeTransition || TransitionUtil.isOpeningType(mode); } } + return null; } @Override 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 5a6ea214e561..cf139a008164 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 @@ -103,6 +103,7 @@ import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -258,6 +259,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final RecentsTransitionHandler mRecentsTransitionHandler; private final DesktopModeCompatPolicy mDesktopModeCompatPolicy; private final DesktopTilingDecorViewModel mDesktopTilingDecorViewModel; + private final MultiDisplayDragMoveIndicatorController mMultiDisplayDragMoveIndicatorController; public DesktopModeWindowDecorViewModel( Context context, @@ -296,7 +298,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, - DesktopTilingDecorViewModel desktopTilingDecorViewModel) { + DesktopTilingDecorViewModel desktopTilingDecorViewModel, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { this( context, shellExecutor, @@ -340,7 +343,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, - desktopTilingDecorViewModel); + desktopTilingDecorViewModel, + multiDisplayDragMoveIndicatorController); } @VisibleForTesting @@ -387,7 +391,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, - DesktopTilingDecorViewModel desktopTilingDecorViewModel) { + DesktopTilingDecorViewModel desktopTilingDecorViewModel, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -460,6 +465,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopModeCompatPolicy = desktopModeCompatPolicy; mDesktopTilingDecorViewModel = desktopTilingDecorViewModel; mDesktopTasksController.setSnapEventHandler(this); + mMultiDisplayDragMoveIndicatorController = multiDisplayDragMoveIndicatorController; shellInit.addInitCallback(this::onInit, this); } @@ -1759,7 +1765,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mTransitions, mInteractionJankMonitor, mTransactionFactory, - mMainHandler); + mMainHandler, + mMultiDisplayDragMoveIndicatorController); windowDecoration.setTaskDragResizer(taskPositioner); final DesktopModeTouchEventListener touchEventListener = @@ -2056,7 +2063,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Transitions transitions, InteractionJankMonitor interactionJankMonitor, Supplier<SurfaceControl.Transaction> transactionFactory, - Handler handler) { + Handler handler, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { final TaskPositioner taskPositioner = DesktopModeStatus.isVeiledResizeEnabled() // TODO(b/383632995): Update when the flag is launched. ? (Flags.enableConnectedDisplaysWindowDrag() @@ -2067,7 +2075,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, dragEventListener, transitions, interactionJankMonitor, - handler) + handler, + multiDisplayDragMoveIndicatorController) : new VeiledResizeTaskPositioner( taskOrganizer, windowDecoration, 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 dca376f7df0e..6165dbf686fd 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 @@ -1069,7 +1069,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private static int getCornerRadius(@NonNull Context context, int layoutResId) { if (layoutResId == R.layout.desktop_mode_app_header) { return loadDimensionPixelSize(context.getResources(), - R.dimen.desktop_windowing_freeform_rounded_corner_radius); + com.android.wm.shell.shared.R.dimen + .desktop_windowing_freeform_rounded_corner_radius); } return INVALID_CORNER_RADIUS; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt index bb20292a51d4..c6cb62d153ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor import android.graphics.PointF import android.graphics.Rect +import android.hardware.display.DisplayTopology import android.os.Handler import android.os.IBinder import android.os.Looper @@ -32,10 +33,10 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.MultiDisplayDragMoveBoundsCalculator +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.transition.Transitions import java.util.concurrent.TimeUnit -import java.util.function.Supplier /** * A task positioner that also takes into account resizing a @@ -49,11 +50,12 @@ class MultiDisplayVeiledResizeTaskPositioner( private val desktopWindowDecoration: DesktopModeWindowDecoration, private val displayController: DisplayController, dragEventListener: DragPositioningCallbackUtility.DragEventListener, - private val transactionSupplier: Supplier<SurfaceControl.Transaction>, + private val transactionSupplier: () -> SurfaceControl.Transaction, private val transitions: Transitions, private val interactionJankMonitor: InteractionJankMonitor, @ShellMainThread private val handler: Handler, -) : TaskPositioner, Transitions.TransitionHandler { + private val multiDisplayDragMoveIndicatorController: MultiDisplayDragMoveIndicatorController, +) : TaskPositioner, Transitions.TransitionHandler, DisplayController.OnDisplaysChangedListener { private val dragEventListeners = mutableListOf<DragPositioningCallbackUtility.DragEventListener>() private val stableBounds = Rect() @@ -71,6 +73,7 @@ class MultiDisplayVeiledResizeTaskPositioner( private var isResizingOrAnimatingResize = false @Surface.Rotation private var rotation = 0 private var startDisplayId = 0 + private val displayIds = mutableSetOf<Int>() constructor( taskOrganizer: ShellTaskOrganizer, @@ -80,19 +83,22 @@ class MultiDisplayVeiledResizeTaskPositioner( transitions: Transitions, interactionJankMonitor: InteractionJankMonitor, @ShellMainThread handler: Handler, + multiDisplayDragMoveIndicatorController: MultiDisplayDragMoveIndicatorController, ) : this( taskOrganizer, windowDecoration, displayController, dragEventListener, - Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() }, + { SurfaceControl.Transaction() }, transitions, interactionJankMonitor, handler, + multiDisplayDragMoveIndicatorController, ) init { dragEventListeners.add(dragEventListener) + displayController.addDisplayWindowListener(this) } override fun onDragPositioningStart(ctrlType: Int, displayId: Int, x: Float, y: Float): Rect { @@ -164,7 +170,7 @@ class MultiDisplayVeiledResizeTaskPositioner( createLongTimeoutJankConfigBuilder(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) ) - val t = transactionSupplier.get() + val t = transactionSupplier() val startDisplayLayout = displayController.getDisplayLayout(startDisplayId) val currentDisplayLayout = displayController.getDisplayLayout(displayId) @@ -196,7 +202,13 @@ class MultiDisplayVeiledResizeTaskPositioner( ) ) - // TODO(b/383069173): Render drag indicator(s) + multiDisplayDragMoveIndicatorController.onDragMove( + boundsDp, + startDisplayId, + desktopWindowDecoration.mTaskInfo, + displayIds, + transactionSupplier, + ) t.setPosition( desktopWindowDecoration.leash, @@ -267,7 +279,10 @@ class MultiDisplayVeiledResizeTaskPositioner( ) ) - // TODO(b/383069173): Clear drag indicator(s) + multiDisplayDragMoveIndicatorController.onDragEnd( + desktopWindowDecoration.mTaskInfo.taskId, + transactionSupplier, + ) } interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) @@ -348,6 +363,14 @@ class MultiDisplayVeiledResizeTaskPositioner( dragEventListeners.remove(dragEventListener) } + override fun onTopologyChanged(topology: DisplayTopology) { + // TODO: b/383069173 - Cancel window drag when topology changes happen during drag. + + displayIds.clear() + val displayBounds = topology.getAbsoluteBounds() + displayIds.addAll(List(displayBounds.size()) { displayBounds.keyAt(it) }) + } + companion object { // Timeout used for resize and drag CUJs, this is longer than the default timeout to avoid // timing out in the middle of a resize or drag action. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt index 88cc94ca8728..7af6b8e26cbf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt @@ -90,6 +90,7 @@ public class ResizeVeil @JvmOverloads constructor( private var viewHost: SurfaceControlViewHost? = null private var display: Display? = null private var veilAnimator: ValueAnimator? = null + private var iconAnimator: ValueAnimator? = null private var loadAppInfoJob: Job? = null /** @@ -241,7 +242,7 @@ public class ResizeVeil @JvmOverloads constructor( } }) } - val iconAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + iconAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = RESIZE_ALPHA_DURATION addUpdateListener { iconAnimT.setAlpha(icon, animatedValue as Float) @@ -265,7 +266,7 @@ public class ResizeVeil @JvmOverloads constructor( .hide(background) .apply() veilAnimator?.start() - iconAnimator.start() + iconAnimator?.start() } else { // Show the veil immediately. t.apply() @@ -414,6 +415,10 @@ public class ResizeVeil @JvmOverloads constructor( private fun cancelAnimation() { veilAnimator?.removeAllUpdateListeners() veilAnimator?.cancel() + veilAnimator = null + iconAnimator?.removeAllUpdateListeners() + iconAnimator?.cancel() + iconAnimator = null } /** @@ -421,7 +426,6 @@ public class ResizeVeil @JvmOverloads constructor( */ fun dispose() { cancelAnimation() - veilAnimator = null isVisible = false loadAppInfoJob?.cancel() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt new file mode 100644 index 000000000000..abd238847519 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2025 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.common + +import android.app.ActivityManager.RunningTaskInfo +import android.content.res.Configuration +import android.graphics.Rect +import android.graphics.RectF +import android.testing.TestableResources +import android.view.Display +import android.view.SurfaceControl +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import java.util.function.Supplier +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.whenever + +/** + * Tests for [MultiDisplayDragMoveIndicatorController]. + * + * Build/Install/Run: atest WMShellUnitTests:MultiDisplayDragMoveIndicatorControllerTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class MultiDisplayDragMoveIndicatorControllerTest : ShellTestCase() { + private val displayController = mock<DisplayController>() + private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() + private val indicatorSurfaceFactory = mock<MultiDisplayDragMoveIndicatorSurface.Factory>() + private val indicatorSurface0 = mock<MultiDisplayDragMoveIndicatorSurface>() + private val indicatorSurface1 = mock<MultiDisplayDragMoveIndicatorSurface>() + private val transaction = mock<SurfaceControl.Transaction>() + private val transactionSupplier = mock<Supplier<SurfaceControl.Transaction>>() + private val taskInfo = mock<RunningTaskInfo>() + private val display0 = mock<Display>() + private val display1 = mock<Display>() + + private lateinit var resources: TestableResources + private val executor = TestShellExecutor() + + private lateinit var controller: MultiDisplayDragMoveIndicatorController + + @Before + fun setUp() { + resources = mContext.getOrCreateTestableResources() + val resourceConfiguration = Configuration() + resourceConfiguration.uiMode = 0 + resources.overrideConfiguration(resourceConfiguration) + + controller = + MultiDisplayDragMoveIndicatorController( + displayController, + rootTaskDisplayAreaOrganizer, + indicatorSurfaceFactory, + executor, + ) + + val spyDisplayLayout0 = + MultiDisplayTestUtil.createSpyDisplayLayout( + MultiDisplayTestUtil.DISPLAY_GLOBAL_BOUNDS_0, + MultiDisplayTestUtil.DISPLAY_DPI_0, + resources.resources, + ) + val spyDisplayLayout1 = + MultiDisplayTestUtil.createSpyDisplayLayout( + MultiDisplayTestUtil.DISPLAY_GLOBAL_BOUNDS_1, + MultiDisplayTestUtil.DISPLAY_DPI_1, + resources.resources, + ) + + taskInfo.taskId = TASK_ID + whenever(displayController.getDisplayLayout(0)).thenReturn(spyDisplayLayout0) + whenever(displayController.getDisplayLayout(1)).thenReturn(spyDisplayLayout1) + whenever(displayController.getDisplay(0)).thenReturn(display0) + whenever(displayController.getDisplay(1)).thenReturn(display1) + whenever(indicatorSurfaceFactory.create(taskInfo, display0)).thenReturn(indicatorSurface0) + whenever(indicatorSurfaceFactory.create(taskInfo, display1)).thenReturn(indicatorSurface1) + whenever(transactionSupplier.get()).thenReturn(transaction) + } + + @Test + fun onDrag_boundsNotIntersectWithDisplay_noIndicator() { + controller.onDragMove( + RectF(2000f, 2000f, 2100f, 2200f), // not intersect with any display + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1), + ) { transaction } + executor.flushAll() + + verify(indicatorSurfaceFactory, never()).create(any(), any()) + } + + @Test + fun onDrag_boundsIntersectWithStartDisplay_noIndicator() { + controller.onDragMove( + RectF(100f, 100f, 200f, 200f), // intersect with display 0 + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1), + ) { transaction } + executor.flushAll() + + verify(indicatorSurfaceFactory, never()).create(any(), any()) + } + + @Test + fun onDrag_boundsIntersectWithNonStartDisplay_showAndDisposeIndicator() { + controller.onDragMove( + RectF(100f, -100f, 200f, 200f), // intersect with display 0 and 1 + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1), + ) { transaction } + executor.flushAll() + + verify(indicatorSurfaceFactory, times(1)).create(taskInfo, display1) + verify(indicatorSurface1, times(1)) + .show(transaction, taskInfo, rootTaskDisplayAreaOrganizer, 1, Rect(0, 1800, 200, 2400)) + + controller.onDragMove( + RectF(2000f, 2000f, 2100f, 2200f), // not intersect with display 1 + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1) + ) { transaction } + while (executor.callbacks.isNotEmpty()) { + executor.flushAll() + } + + verify(indicatorSurface1, times(1)) + .relayout(any(), eq(transaction), shouldBeVisible = eq(false)) + + controller.onDragEnd(TASK_ID, { transaction }) + while (executor.callbacks.isNotEmpty()) { + executor.flushAll() + } + + verify(indicatorSurface1, times(1)).disposeSurface(transaction) + } + + companion object { + private const val TASK_ID = 10 + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index 0d5741fccbcc..8ad54f5a0bb4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -16,51 +16,28 @@ package com.android.wm.shell.desktopmode -import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM -import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN -import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED -import android.content.ContentResolver -import android.os.Binder import android.platform.test.annotations.EnableFlags -import android.provider.Settings -import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY -import android.view.IWindowManager -import android.view.WindowManager.TRANSIT_CHANGE -import android.window.DisplayAreaInfo -import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.window.flags.Flags -import com.android.wm.shell.MockToken -import com.android.wm.shell.RootTaskDisplayAreaOrganizer -import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit -import com.android.wm.shell.transition.Transitions -import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock -import org.mockito.Mockito.anyInt import org.mockito.Mockito.spy -import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -73,27 +50,18 @@ import org.mockito.quality.Strictness @RunWith(AndroidTestingRunner::class) class DesktopDisplayEventHandlerTest : ShellTestCase() { @Mock lateinit var testExecutor: ShellExecutor - @Mock lateinit var transitions: Transitions @Mock lateinit var displayController: DisplayController - @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer - @Mock private lateinit var mockWindowManager: IWindowManager @Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories @Mock private lateinit var mockDesktopRepository: DesktopRepository @Mock private lateinit var mockDesktopTasksController: DesktopTasksController - @Mock private lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock private lateinit var desktopDisplayModeController: DesktopDisplayModeController private lateinit var mockitoSession: StaticMockitoSession private lateinit var shellInit: ShellInit private lateinit var handler: DesktopDisplayEventHandler private val onDisplaysChangedListenerCaptor = argumentCaptor<OnDisplaysChangedListener>() - private val runningTasks = mutableListOf<RunningTaskInfo>() private val externalDisplayId = 100 - private val freeformTask = - TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build() - private val fullscreenTask = - TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build() - private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) @Before fun setUp() { @@ -105,24 +73,15 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { shellInit = spy(ShellInit(testExecutor)) whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository) - whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) - .thenReturn(defaultTDA) handler = DesktopDisplayEventHandler( context, shellInit, - transitions, displayController, - rootTaskDisplayAreaOrganizer, - mockWindowManager, mockDesktopUserRepositories, mockDesktopTasksController, - shellTaskOrganizer, + desktopDisplayModeController, ) - whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } - runningTasks.add(freeformTask) - runningTasks.add(fullscreenTask) shellInit.init() verify(displayController) .addDisplayWindowListener(onDisplaysChangedListenerCaptor.capture()) @@ -133,65 +92,6 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { mockitoSession.finishMocking() } - private fun testDisplayWindowingModeSwitch( - defaultWindowingMode: Int, - extendedDisplayEnabled: Boolean, - expectTransition: Boolean, - ) { - defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode - whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode } - val settingsSession = - ExtendedDisplaySettingsSession( - context.contentResolver, - if (extendedDisplayEnabled) 1 else 0, - ) - - settingsSession.use { - connectExternalDisplay() - defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - disconnectExternalDisplay() - - if (expectTransition) { - val arg = argumentCaptor<WindowContainerTransaction>() - verify(transitions, times(2)) - .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) - assertThat(arg.firstValue.changes[defaultTDA.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode) - .isEqualTo(defaultWindowingMode) - } else { - verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull()) - } - } - } - - @Test - fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() { - testDisplayWindowingModeSwitch( - defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, - extendedDisplayEnabled = false, - expectTransition = false, - ) - } - - @Test - fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() { - testDisplayWindowingModeSwitch( - defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, - extendedDisplayEnabled = true, - expectTransition = true, - ) - } - - @Test - fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() { - testDisplayWindowingModeSwitch( - defaultWindowingMode = WINDOWING_MODE_FREEFORM, - extendedDisplayEnabled = true, - expectTransition = false, - ) - } - @Test fun testDisplayAdded_supportsDesks_createsDesk() { whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) @@ -231,70 +131,14 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { } @Test - fun displayWindowingModeSwitch_existingTasksOnConnected() { - defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { - WINDOWING_MODE_FULLSCREEN - } - - ExtendedDisplaySettingsSession(context.contentResolver, 1).use { - connectExternalDisplay() - - val arg = argumentCaptor<WindowContainerTransaction>() - verify(transitions, times(1)) - .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) - assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - } - } - - @Test - fun displayWindowingModeSwitch_existingTasksOnDisconnected() { - defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { - WINDOWING_MODE_FULLSCREEN - } - - ExtendedDisplaySettingsSession(context.contentResolver, 1).use { - disconnectExternalDisplay() - - val arg = argumentCaptor<WindowContainerTransaction>() - verify(transitions, times(1)) - .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) - assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - } - - private fun connectExternalDisplay() { - whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId)) + fun testConnectExternalDisplay() { onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(externalDisplayId) + verify(desktopDisplayModeController).refreshDisplayWindowingMode() } - private fun disconnectExternalDisplay() { - whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) - .thenReturn(intArrayOf(DEFAULT_DISPLAY)) + @Test + fun testDisconnectExternalDisplay() { onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId) - } - - private class ExtendedDisplaySettingsSession( - private val contentResolver: ContentResolver, - private val overrideValue: Int, - ) : AutoCloseable { - private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS - private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0) - - init { - Settings.Global.putInt(contentResolver, settingName, overrideValue) - } - - override fun close() { - Settings.Global.putInt(contentResolver, settingName, initialValue) - } + verify(desktopDisplayModeController).refreshDisplayWindowingMode() } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt new file mode 100644 index 000000000000..0ff7230f6e0c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.content.ContentResolver +import android.os.Binder +import android.provider.Settings +import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import android.view.IWindowManager +import android.view.WindowManager.TRANSIT_CHANGE +import android.window.DisplayAreaInfo +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.never +import com.android.wm.shell.MockToken +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider +import com.android.wm.shell.transition.Transitions +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.isNull +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** + * Test class for [DesktopDisplayModeController] + * + * Usage: atest WMShellUnitTests:DesktopDisplayModeControllerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopDisplayModeControllerTest : ShellTestCase() { + private val transitions = mock<Transitions>() + private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() + private val mockWindowManager = mock<IWindowManager>() + private val shellTaskOrganizer = mock<ShellTaskOrganizer>() + private val desktopWallpaperActivityTokenProvider = + mock<DesktopWallpaperActivityTokenProvider>() + + private lateinit var controller: DesktopDisplayModeController + + private val runningTasks = mutableListOf<RunningTaskInfo>() + private val freeformTask = + TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build() + private val fullscreenTask = + TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build() + private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + private val wallpaperToken = MockToken().token() + + @Before + fun setUp() { + whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder()) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .thenReturn(defaultTDA) + controller = + DesktopDisplayModeController( + context, + transitions, + rootTaskDisplayAreaOrganizer, + mockWindowManager, + shellTaskOrganizer, + desktopWallpaperActivityTokenProvider, + ) + runningTasks.add(freeformTask) + runningTasks.add(fullscreenTask) + whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(ArrayList(runningTasks)) + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken) + } + + private fun testDisplayWindowingModeSwitch( + defaultWindowingMode: Int, + extendedDisplayEnabled: Boolean, + expectTransition: Boolean, + ) { + defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode + whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode) + val settingsSession = + ExtendedDisplaySettingsSession( + context.contentResolver, + if (extendedDisplayEnabled) 1 else 0, + ) + + settingsSession.use { + connectExternalDisplay() + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + disconnectExternalDisplay() + + if (expectTransition) { + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(2)) + .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[defaultTDA.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(arg.firstValue.changes[wallpaperToken.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode) + .isEqualTo(defaultWindowingMode) + assertThat(arg.secondValue.changes[wallpaperToken.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } else { + verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull()) + } + } + } + + @Test + fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() { + testDisplayWindowingModeSwitch( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + extendedDisplayEnabled = false, + expectTransition = false, + ) + } + + @Test + fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() { + testDisplayWindowingModeSwitch( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + extendedDisplayEnabled = true, + expectTransition = true, + ) + } + + @Test + fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() { + testDisplayWindowingModeSwitch( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + extendedDisplayEnabled = true, + expectTransition = false, + ) + } + + @Test + fun displayWindowingModeSwitch_existingTasksOnConnected() { + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(WINDOWING_MODE_FULLSCREEN) + + ExtendedDisplaySettingsSession(context.contentResolver, 1).use { + connectExternalDisplay() + + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(1)) + .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } + } + + @Test + fun displayWindowingModeSwitch_existingTasksOnDisconnected() { + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { + WINDOWING_MODE_FULLSCREEN + } + + ExtendedDisplaySettingsSession(context.contentResolver, 1).use { + disconnectExternalDisplay() + + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(1)) + .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + } + + private fun connectExternalDisplay() { + whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, EXTERNAL_DISPLAY_ID)) + controller.refreshDisplayWindowingMode() + } + + private fun disconnectExternalDisplay() { + whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) + .thenReturn(intArrayOf(DEFAULT_DISPLAY)) + controller.refreshDisplayWindowingMode() + } + + private class ExtendedDisplaySettingsSession( + private val contentResolver: ContentResolver, + private val overrideValue: Int, + ) : AutoCloseable { + private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS + private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0) + + init { + Settings.Global.putInt(contentResolver, settingName, overrideValue) + } + + override fun close() { + Settings.Global.putInt(contentResolver, settingName, initialValue) + } + } + + private companion object { + const val EXTERNAL_DISPLAY_ID = 100 + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index ed9b97d264f7..9bff287e314a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -333,7 +333,7 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { @Test fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() { repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) - repo.addClosingTask(DEFAULT_DISPLAY, 1) + repo.addClosingTask(displayId = DEFAULT_DISPLAY, deskId = 0, taskId = 1) // A visible task that's closing assertThat(repo.isVisibleTask(1)).isTrue() 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 fcd92ac2678a..2e63c4f51792 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 @@ -2827,7 +2827,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun onDesktopWindowClose_singleActiveTask_isClosing() { val task = setUpFreeformTask() - taskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId) + taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, deskId = 0, taskId = task.taskId) val wct = WindowContainerTransaction() controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) @@ -2864,7 +2864,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() - taskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId) + taskRepository.addClosingTask( + displayId = DEFAULT_DISPLAY, + deskId = 0, + taskId = task2.taskId, + ) val wct = WindowContainerTransaction() controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1) @@ -3225,6 +3229,30 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowMinimize_minimizesTask() { + val task = setUpFreeformTask() + val transition = Binder() + val runOnTransit = RunOnStartTransitionCallback() + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn(transition) + whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any())) + .thenReturn( + ExitResult.Exit(exitingTask = task.taskId, runOnTransitionStart = runOnTransit) + ) + + controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + + verify(desksOrganizer).minimizeTask(any(), /* deskId= */ eq(0), eq(task)) + } + + @Test fun onDesktopWindowMinimize_triesToStopTiling() { val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val transition = Binder() @@ -3972,7 +4000,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + taskRepository.addClosingTask( + displayId = DEFAULT_DISPLAY, + deskId = 0, + taskId = task2.taskId, + ) val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) @@ -4083,6 +4115,36 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_closeTransition_onlyDesktopTask_deactivatesDesk() { + val task = setUpFreeformTask() + + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + verify(desksOrganizer).deactivateDesk(any(), /* deskId= */ eq(0)) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_closeTransition_onlyDesktopTask_addsDeactivatesDeskTransition() { + val transition = Binder() + val task = setUpFreeformTask() + + controller.handleRequest(transition, createTransition(task, type = TRANSIT_CLOSE)) + + verify(desksTransitionsObserver) + .addPendingTransition(DeskTransition.DeactivateDesk(token = transition, deskId = 0)) + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() { val task1 = setUpFreeformTask() @@ -4115,7 +4177,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + taskRepository.addClosingTask( + displayId = DEFAULT_DISPLAY, + deskId = 0, + taskId = task2.taskId, + ) val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index 8b10ca1a2a70..96b85ad2729e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -22,6 +22,7 @@ import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.Change import android.window.WindowContainerTransaction.HierarchyOp import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT import androidx.test.filters.SmallTest @@ -29,15 +30,19 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskMinimizationRoot import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat +import kotlin.test.assertNotNull import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.kotlin.argThat import org.mockito.kotlin.mock /** @@ -75,6 +80,43 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test + fun testCreateDesk_createsMinimizationRoot() { + val callback = FakeOnCreateCallback() + organizer.createDesk(Display.DEFAULT_DISPLAY, callback) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + + val minimizationRoot = organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId] + assertNotNull(minimizationRoot) + assertThat(minimizationRoot.deskId).isEqualTo(freeformRoot.taskId) + assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId) + } + + @Test + fun testCreateMinimizationRoot_marksHidden() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + + verify(mockShellTaskOrganizer) + .applyTransaction( + argThat { wct -> + wct.changes.any { change -> + change.key == minimizationRootTask.token.asBinder() && + (change.value.changeMask and Change.CHANGE_HIDDEN != 0) && + change.value.hidden + } + } + ) + } + + @Test fun testOnTaskAppeared_withoutRequest_throws() { val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } @@ -105,57 +147,122 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test + fun testOnTaskAppeared_duplicateMinimizedRoot_throws() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + + assertThrows(Exception::class.java) { + organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + } + } + + @Test fun testOnTaskVanished_removesRoot() { val desk = createDesk() - organizer.onTaskVanished(desk.taskInfo) + organizer.onTaskVanished(desk.deskRoot.taskInfo) + + assertThat(organizer.deskRootsByDeskId.contains(desk.deskRoot.deskId)).isFalse() + } + + @Test + fun testOnTaskVanished_removesMinimizedRoot() { + val desk = createDesk() + + organizer.onTaskVanished(desk.deskRoot.taskInfo) + organizer.onTaskVanished(desk.minimizationRoot.taskInfo) - assertThat(organizer.roots.contains(desk.deskId)).isFalse() + assertThat(organizer.deskMinimizationRootsByDeskId.contains(desk.deskRoot.deskId)).isFalse() } @Test fun testDesktopWindowAppearsInDesk() { val desk = createDesk() - val child = createFreeformTask().apply { parentTaskId = desk.deskId } + val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) - assertThat(desk.children).contains(child.taskId) + assertThat(desk.deskRoot.children).contains(child.taskId) + } + + @Test + fun testDesktopWindowAppearsInDeskMinimizationRoot() { + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } + + organizer.onTaskAppeared(child, SurfaceControl()) + + assertThat(desk.minimizationRoot.children).contains(child.taskId) + } + + @Test + fun testDesktopWindowMovesToMinimizationRoot() { + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + organizer.onTaskAppeared(child, SurfaceControl()) + + child.parentTaskId = desk.minimizationRoot.rootId + organizer.onTaskInfoChanged(child) + + assertThat(desk.deskRoot.children).doesNotContain(child.taskId) + assertThat(desk.minimizationRoot.children).contains(child.taskId) } @Test fun testDesktopWindowDisappearsFromDesk() { val desk = createDesk() - val child = createFreeformTask().apply { parentTaskId = desk.deskId } + val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) organizer.onTaskVanished(child) - assertThat(desk.children).doesNotContain(child.taskId) + assertThat(desk.deskRoot.children).doesNotContain(child.taskId) } @Test - fun testRemoveDesk() { + fun testDesktopWindowDisappearsFromDeskMinimizationRoot() { + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } + + organizer.onTaskAppeared(child, SurfaceControl()) + organizer.onTaskVanished(child) + + assertThat(desk.minimizationRoot.children).doesNotContain(child.taskId) + } + + @Test + fun testRemoveDesk_removesDeskRoot() { val desk = createDesk() val wct = WindowContainerTransaction() - organizer.removeDesk(wct, desk.deskId) + organizer.removeDesk(wct, desk.deskRoot.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && - hop.container == desk.taskInfo.token.asBinder() + hop.container == desk.deskRoot.token.asBinder() } ) .isTrue() } @Test - fun testRemoveDesk_didNotExist_throws() { - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + fun testRemoveDesk_removesMinimizationRoot() { + val desk = createDesk() val wct = WindowContainerTransaction() - assertThrows(Exception::class.java) { organizer.removeDesk(wct, freeformRoot.taskId) } + organizer.removeDesk(wct, desk.deskRoot.deskId) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && + hop.container == desk.minimizationRoot.token.asBinder() + } + ) + .isTrue() } @Test @@ -163,20 +270,20 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { val desk = createDesk() val wct = WindowContainerTransaction() - organizer.activateDesk(wct, desk.deskId) + organizer.activateDesk(wct, desk.deskRoot.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER && hop.toTop && - hop.container == desk.taskInfo.token.asBinder() + hop.container == desk.deskRoot.taskInfo.token.asBinder() } ) .isTrue() assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && - hop.container == desk.taskInfo.token.asBinder() + hop.container == desk.deskRoot.taskInfo.token.asBinder() } ) .isTrue() @@ -196,14 +303,14 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { val desktopTask = createFreeformTask().apply { parentTaskId = -1 } val wct = WindowContainerTransaction() - organizer.moveTaskToDesk(wct, desk.deskId, desktopTask) + organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, desktopTask) assertThat( wct.hierarchyOps.any { hop -> hop.isReparent && hop.toTop && hop.container == desktopTask.token.asBinder() && - hop.newParent == desk.taskInfo.token.asBinder() + hop.newParent == desk.deskRoot.taskInfo.token.asBinder() } ) .isTrue() @@ -231,13 +338,26 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun testGetDeskAtEnd() { val desk = createDesk() - val task = createFreeformTask().apply { parentTaskId = desk.deskId } + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val endDesk = + organizer.getDeskAtEnd( + TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } + ) + + assertThat(endDesk).isEqualTo(desk.deskRoot.deskId) + } + + @Test + fun testGetDeskAtEnd_inMinimizationRoot() { + val desk = createDesk() + + val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } val endDesk = organizer.getDeskAtEnd( TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } ) - assertThat(endDesk).isEqualTo(desk.deskId) + assertThat(endDesk).isEqualTo(desk.deskRoot.deskId) } @Test @@ -264,14 +384,14 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun deactivateDesk_clearsLaunchRoot() { val wct = WindowContainerTransaction() val desk = createDesk() - organizer.activateDesk(wct, desk.deskId) + organizer.activateDesk(wct, desk.deskRoot.deskId) - organizer.deactivateDesk(wct, desk.deskId) + organizer.deactivateDesk(wct, desk.deskRoot.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && - hop.container == desk.taskInfo.token.asBinder() && + hop.container == desk.deskRoot.taskInfo.token.asBinder() && hop.windowingModes == null && hop.activityTypes == null } @@ -280,25 +400,129 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun isDeskChange() { + fun isDeskChange_forDeskId() { val desk = createDesk() assertThat( organizer.isDeskChange( - TransitionInfo.Change(desk.taskInfo.token, desk.leash).apply { - taskInfo = desk.taskInfo + TransitionInfo.Change(desk.deskRoot.taskInfo.token, desk.deskRoot.leash).apply { + taskInfo = desk.deskRoot.taskInfo }, - desk.deskId, + desk.deskRoot.deskId, + ) + ) + .isTrue() + } + + @Test + fun isDeskChange_forDeskId_inMinimizationRoot() { + val desk = createDesk() + + assertThat( + organizer.isDeskChange( + change = + TransitionInfo.Change( + desk.minimizationRoot.token, + desk.minimizationRoot.leash, + ) + .apply { taskInfo = desk.minimizationRoot.taskInfo }, + deskId = desk.deskRoot.deskId, + ) + ) + .isTrue() + } + + @Test + fun isDeskChange_anyDesk() { + val desk = createDesk() + + assertThat( + organizer.isDeskChange( + change = + TransitionInfo.Change(desk.deskRoot.taskInfo.token, desk.deskRoot.leash) + .apply { taskInfo = desk.deskRoot.taskInfo } + ) + ) + .isTrue() + } + + @Test + fun isDeskChange_anyDesk_inMinimizationRoot() { + val desk = createDesk() + + assertThat( + organizer.isDeskChange( + change = + TransitionInfo.Change( + desk.minimizationRoot.taskInfo.token, + desk.minimizationRoot.leash, + ) + .apply { taskInfo = desk.minimizationRoot.taskInfo } ) ) .isTrue() } - private fun createDesk(): DeskRoot { + @Test + fun minimizeTask() { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) + organizer.onTaskAppeared(task, SurfaceControl()) + + organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.isReparent && + hop.container == task.token.asBinder() && + hop.newParent == desk.minimizationRoot.token.asBinder() + } + ) + .isTrue() + } + + @Test + fun minimizeTask_alreadyMinimized_noOp() { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } + val wct = WindowContainerTransaction() + organizer.onTaskAppeared(task, SurfaceControl()) + + organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat(wct.isEmpty).isTrue() + } + + @Test + fun minimizeTask_inDifferentDesk_noOp() { + val desk = createDesk() + val otherDesk = createDesk() + val task = createFreeformTask().apply { parentTaskId = otherDesk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.onTaskAppeared(task, SurfaceControl()) + + organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat(wct.isEmpty).isTrue() + } + + private data class DeskRoots( + val deskRoot: DeskRoot, + val minimizationRoot: DeskMinimizationRoot, + ) + + private fun createDesk(): DeskRoots { organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - return organizer.roots[freeformRoot.taskId] + val minimizationRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(minimizationRoot, SurfaceControl()) + return DeskRoots( + organizer.deskRootsByDeskId[freeformRoot.taskId], + checkNotNull(organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId]), + ) } private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index fd5e567f69ed..93dd3456f3f2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -17,16 +17,20 @@ package com.android.wm.shell.recents; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; +import static com.android.wm.shell.Flags.FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED; +import static com.android.wm.shell.transition.Transitions.TRANSIT_END_RECENTS_TRANSITION; import static com.android.wm.shell.transition.Transitions.TRANSIT_START_RECENTS_TRANSITION; import static com.google.common.truth.Truth.assertThat; @@ -64,7 +68,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.os.IResultReceiver; -import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; @@ -73,6 +76,7 @@ import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.shared.R; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -308,8 +312,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { mRecentsTransitionHandler.findController(transition).merge( mergeTransitionInfo, new StubTransaction(), - finishT, - transition, + new StubTransaction(), mock(Transitions.TransitionFinishCallback.class)); mMainExecutor.flushAll(); @@ -318,6 +321,69 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { } @Test + @EnableFlags(FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION) + public void testMerge_consumeBookendTransition() throws Exception { + // Start and finish the transition + final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class); + final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner); + mRecentsTransitionHandler.startAnimation( + transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), + mock(Transitions.TransitionFinishCallback.class)); + mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, + false /* sendUserLeaveHint */, mock(IResultReceiver.class)); + mMainExecutor.flushAll(); + + // Merge the bookend transition + TransitionInfo mergeTransitionInfo = + new TransitionInfoBuilder(TRANSIT_END_RECENTS_TRANSITION) + .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build()) + .build(); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + Transitions.TransitionFinishCallback finishCallback + = mock(Transitions.TransitionFinishCallback.class); + mRecentsTransitionHandler.findController(transition).merge( + mergeTransitionInfo, + new StubTransaction(), + finishT, + finishCallback); + mMainExecutor.flushAll(); + + // Verify that we've merged + verify(finishCallback).onTransitionFinished(any()); + } + + @Test + @EnableFlags(FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION) + public void testMerge_pendingBookendTransition_mergesTransition() throws Exception { + // Start and finish the transition + final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class); + final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner); + mRecentsTransitionHandler.startAnimation( + transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), + mock(Transitions.TransitionFinishCallback.class)); + mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, + false /* sendUserLeaveHint */, mock(IResultReceiver.class)); + mMainExecutor.flushAll(); + + // Merge a new transition while we have a pending finish + TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build()) + .build(); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + Transitions.TransitionFinishCallback finishCallback + = mock(Transitions.TransitionFinishCallback.class); + mRecentsTransitionHandler.findController(transition).merge( + mergeTransitionInfo, + new StubTransaction(), + finishT, + finishCallback); + mMainExecutor.flushAll(); + + // Verify that we've cleaned up the original transition + assertNull(mRecentsTransitionHandler.findController(transition)); + } + + @Test @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() { ActivityManager.RunningTaskInfo freeformTask = @@ -336,7 +402,6 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { mergeTransitionInfo, new StubTransaction(), finishT, - transition, mock(Transitions.TransitionFinishCallback.class)); mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, false /* sendUserLeaveHint */, mock(IResultReceiver.class)); @@ -385,15 +450,23 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { } private TransitionInfo createTransitionInfo() { - final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() + final ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder() .setTopActivityType(ACTIVITY_TYPE_HOME) .build(); + final ActivityManager.RunningTaskInfo appTask = new TestRunningTaskInfoBuilder() + .setTopActivityType(ACTIVITY_TYPE_STANDARD) + .build(); final TransitionInfo.Change homeChange = new TransitionInfo.Change( - task.token, new SurfaceControl()); + homeTask.token, new SurfaceControl()); homeChange.setMode(TRANSIT_TO_FRONT); - homeChange.setTaskInfo(task); + homeChange.setTaskInfo(homeTask); + final TransitionInfo.Change appChange = new TransitionInfo.Change( + appTask.token, new SurfaceControl()); + appChange.setMode(TRANSIT_TO_FRONT); + appChange.setTaskInfo(appTask); return new TransitionInfoBuilder(TRANSIT_START_RECENTS_TRANSITION) .addChange(homeChange) + .addChange(appChange) .build(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt index f69bf34ea3f7..88c6e499b869 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt @@ -16,13 +16,16 @@ package com.android.wm.shell.shared.desktopmode +import android.Manifest.permission.SYSTEM_ALERT_WINDOW import android.app.TaskInfo import android.compat.testing.PlatformCompatChangeRule import android.content.ComponentName import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Process +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest @@ -39,7 +42,9 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyString import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -55,6 +60,7 @@ class DesktopModeCompatPolicyTest : ShellTestCase() { private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy private val packageManager: PackageManager = mock() private val homeActivities = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "") + private val baseActivityTest = ComponentName("com.test.dummypackage", "TestClass") @Before fun setUp() { @@ -64,6 +70,7 @@ class DesktopModeCompatPolicyTest : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION) fun testIsTopActivityExemptFromDesktopWindowing_onlyTransparentActivitiesInStack() { assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( createFreeformTask(/* displayId */ 0) @@ -71,10 +78,39 @@ class DesktopModeCompatPolicyTest : ShellTestCase() { isActivityStackTransparent = true isTopActivityNoDisplay = false numActivities = 1 + baseActivity = baseActivityTest })) } @Test + @EnableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION) + fun testIsTopActivityExemptWithPermission_onlyTransparentActivitiesInStack() { + allowOverlayPermission(arrayOf(SYSTEM_ALERT_WINDOW)) + assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( + createFreeformTask(/* displayId */ 0) + .apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = false + numActivities = 1 + baseActivity = baseActivityTest + })) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION) + fun testIsTopActivityExemptWithNoPermission_onlyTransparentActivitiesInStack() { + allowOverlayPermission(arrayOf()) + assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( + createFreeformTask(/* displayId */ 0) + .apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = false + numActivities = 1 + baseActivity = baseActivityTest + })) + } + + @Test fun testIsTopActivityExemptFromDesktopWindowing_noActivitiesInStack() { assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( createFreeformTask(/* displayId */ 0) @@ -219,4 +255,15 @@ class DesktopModeCompatPolicyTest : ShellTestCase() { } } } + + fun allowOverlayPermission(permissions: Array<String>) { + val packageInfo = mock<PackageInfo>() + packageInfo.requestedPermissions = permissions + whenever( + packageManager.getPackageInfo( + anyString(), + eq(PackageManager.GET_PERMISSIONS) + ) + ).thenReturn(packageInfo) + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 6f28e656d060..3099b0f5cf66 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -17,36 +17,44 @@ package com.android.wm.shell.transition; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.WindowConfiguration.ActivityType; import android.content.Context; +import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; +import android.platform.test.annotations.EnableFlags; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.TransitionInfo.TransitionMode; +import android.window.WindowContainerToken; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; @@ -188,6 +196,72 @@ public class HomeTransitionObserverTest extends ShellTestCase { } @Test + @EnableFlags({Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE}) + public void testDragTaskToBubbleOverHome_notifiesHomeIsVisible() throws RemoteException { + ActivityManager.RunningTaskInfo homeTask = createTaskInfo(1, ACTIVITY_TYPE_HOME); + ActivityManager.RunningTaskInfo bubbleTask = createTaskInfo(2, ACTIVITY_TYPE_STANDARD); + + TransitionInfo startDragTransition = + new TransitionInfoBuilder(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP) + .addChange(TRANSIT_TO_FRONT, homeTask) + .addChange(TRANSIT_TO_BACK, bubbleTask) + .build(); + + // Start drag to desktop which brings home to front + mHomeTransitionObserver.onTransitionReady(new Binder(), startDragTransition, + MockTransactionPool.create(), MockTransactionPool.create()); + // Does not notify home visibility yet + verify(mListener, never()).onHomeVisibilityChanged(anyBoolean()); + + TransitionInfo convertToBubbleTransition = + new TransitionInfoBuilder(TRANSIT_CONVERT_TO_BUBBLE) + .addChange(TRANSIT_TO_FRONT, bubbleTask) + .build(); + + // Convert to bubble. Transition does not include changes for home task + mHomeTransitionObserver.onTransitionReady(new Binder(), convertToBubbleTransition, + MockTransactionPool.create(), MockTransactionPool.create()); + + // Notifies home visibility change that was pending from the start of drag + verify(mListener).onHomeVisibilityChanged(true); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE}) + public void testDragTaskToBubbleOverOtherTask_notifiesHomeIsNotVisible() + throws RemoteException { + ActivityManager.RunningTaskInfo homeTask = createTaskInfo(1, ACTIVITY_TYPE_HOME); + ActivityManager.RunningTaskInfo bubbleTask = createTaskInfo(2, ACTIVITY_TYPE_STANDARD); + ActivityManager.RunningTaskInfo otherTask = createTaskInfo(3, ACTIVITY_TYPE_STANDARD); + + TransitionInfo startDragTransition = + new TransitionInfoBuilder(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP) + .addChange(TRANSIT_TO_FRONT, homeTask) + .addChange(TRANSIT_TO_BACK, bubbleTask) + .build(); + + // Start drag to desktop which brings home to front + mHomeTransitionObserver.onTransitionReady(new Binder(), startDragTransition, + MockTransactionPool.create(), MockTransactionPool.create()); + // Does not notify home visibility yet + verify(mListener, never()).onHomeVisibilityChanged(anyBoolean()); + + TransitionInfo convertToBubbleTransition = + new TransitionInfoBuilder(TRANSIT_CONVERT_TO_BUBBLE) + .addChange(TRANSIT_TO_FRONT, bubbleTask) + .addChange(TRANSIT_TO_FRONT, otherTask) + .addChange(TRANSIT_TO_BACK, homeTask) + .build(); + + // Convert to bubble. Transition includes home task to back which updates home visibility + mHomeTransitionObserver.onTransitionReady(new Binder(), convertToBubbleTransition, + MockTransactionPool.create(), MockTransactionPool.create()); + + // Notifies home visibility change due to home moving to back in the second transition + verify(mListener).onHomeVisibilityChanged(false); + } + + @Test public void testHomeActivityWithBackGestureNotifiesHomeIsVisibleAfterClose() throws RemoteException { TransitionInfo info = mock(TransitionInfo.class); @@ -227,4 +301,14 @@ public class HomeTransitionObserverTest extends ShellTestCase { when(change.getMode()).thenReturn(mode); taskInfo.isRunning = isRunning; } + + private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId, int activityType) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.topActivityType = activityType; + taskInfo.configuration.windowConfiguration.setActivityType(activityType); + taskInfo.token = mock(WindowContainerToken.class); + taskInfo.isRunning = true; + return taskInfo; + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index 8cccdb2b6120..81dfaed56b6f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -52,6 +52,7 @@ import com.android.wm.shell.common.DisplayChangeController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler @@ -138,6 +139,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected val mockFreeformTaskTransitionStarter = mock<FreeformTaskTransitionStarter>() protected val mockActivityOrientationChangeHandler = mock<DesktopActivityOrientationChangeHandler>() + protected val mockMultiDisplayDragMoveIndicatorController = + mock<MultiDisplayDragMoveIndicatorController>() protected val mockInputManager = mock<InputManager>() private val mockTaskPositionerFactory = mock<DesktopModeWindowDecorViewModel.TaskPositionerFactory>() @@ -229,6 +232,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockRecentsTransitionHandler, desktopModeCompatPolicy, mockTilingWindowDecoration, + mockMultiDisplayDragMoveIndicatorController, ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -243,6 +247,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { any(), any(), any(), + any(), any() ) ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt index 937938df82c8..a6b077037b86 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt @@ -41,6 +41,7 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController import com.android.wm.shell.common.MultiDisplayTestUtil import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionFinishCallback @@ -62,8 +63,8 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` +import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations /** @@ -93,7 +94,8 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { @Mock private lateinit var mockTransitions: Transitions @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var mockSurfaceControl: SurfaceControl - + @Mock private lateinit var mockMultiDisplayDragMoveIndicatorController: + MultiDisplayDragMoveIndicatorController private lateinit var resources: TestableResources private lateinit var spyDisplayLayout0: DisplayLayout private lateinit var spyDisplayLayout1: DisplayLayout @@ -170,10 +172,11 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { mockDesktopWindowDecoration, mockDisplayController, mockDragEventListener, - mockTransactionFactory, + { mockTransaction }, mockTransitions, mockInteractionJankMonitor, mainHandler, + mockMultiDisplayDragMoveIndicatorController, ) } diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index f5e10d94452f..7a51c20f7672 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -44,6 +44,9 @@ namespace android { namespace { +constexpr int32_t kDefaultDisplayId = 0; +constexpr int32_t kDefaultDeviceId = 0; + using EntryValue = std::variant<Res_value, incfs::verified_map_ptr<ResTable_map_entry>>; /* NOTE: table_entry has been verified in LoadedPackage::GetEntryFromOffset(), @@ -61,7 +64,7 @@ base::expected<EntryValue, IOError> GetEntryValue( return table_entry->value(); } -} // namespace +} // namespace struct FindEntryResult { // The cookie representing the ApkAssets in which the value resides. @@ -99,14 +102,15 @@ struct Theme::Entry { Res_value value; }; -AssetManager2::AssetManager2(ApkAssetsList apk_assets, const ResTable_config& configuration) { +AssetManager2::AssetManager2(ApkAssetsList apk_assets, const ResTable_config& configuration) + : display_id_(kDefaultDisplayId), device_id_(kDefaultDeviceId) { configurations_.push_back(configuration); // Don't invalidate caches here as there's nothing cached yet. SetApkAssets(apk_assets, false); } -AssetManager2::AssetManager2() { +AssetManager2::AssetManager2() : display_id_(kDefaultDisplayId), device_id_(kDefaultDeviceId) { configurations_.emplace_back(); } @@ -172,8 +176,7 @@ void AssetManager2::BuildDynamicRefTable(ApkAssetsList apk_assets) { // to take effect. auto iter = target_assets_package_ids.find(loaded_idmap->TargetApkPath()); if (iter == target_assets_package_ids.end()) { - LOG(INFO) << "failed to find target package for overlay " - << loaded_idmap->OverlayApkPath(); + LOG(INFO) << "failed to find target package for overlay " << loaded_idmap->OverlayApkPath(); } else { uint8_t target_package_id = iter->second; @@ -189,10 +192,11 @@ void AssetManager2::BuildDynamicRefTable(ApkAssetsList apk_assets) { << " assigned package group"; PackageGroup& target_package_group = package_groups_[target_idx]; - target_package_group.overlays_.push_back( - ConfiguredOverlay{loaded_idmap->GetTargetResourcesMap(target_package_id, - overlay_ref_table.get()), - apk_assets_cookies[apk_assets]}); + target_package_group.overlays_.push_back(ConfiguredOverlay{ + loaded_idmap->GetTargetResourcesMap(target_package_id, overlay_ref_table.get()), + apk_assets_cookies[apk_assets], + IsAnyOverlayConstraintSatisfied(loaded_idmap->GetConstraints()) + }); } } @@ -291,7 +295,7 @@ void AssetManager2::DumpToLog() const { } LOG(INFO) << "Package ID map: " << list; - for (const auto& package_group: package_groups_) { + for (const auto& package_group : package_groups_) { list = ""; for (const auto& package : package_group.packages_) { const LoadedPackage* loaded_package = package.loaded_package_; @@ -347,7 +351,6 @@ std::shared_ptr<const DynamicRefTable> AssetManager2::GetDynamicRefTableForCooki const std::unordered_map<std::string, std::string>* AssetManager2::GetOverlayableMapForPackage(uint32_t package_id) const { - if (package_id >= package_ids_.size()) { return nullptr; } @@ -462,6 +465,28 @@ void AssetManager2::SetConfigurations(std::span<const ResTable_config> configura } } +void AssetManager2::SetOverlayConstraints(int32_t display_id, int32_t device_id) { + bool changed = false; + if (display_id_ != display_id) { + display_id_ = display_id; + changed = true; + } + if (device_id_ != device_id) { + device_id_ = device_id; + changed = true; + } + if (changed) { + // Enable/disable overlays based on current constraints + for (PackageGroup& group : package_groups_) { + for (auto &overlay: group.overlays_) { + overlay.enabled = IsAnyOverlayConstraintSatisfied( + overlay.overlay_res_maps_.GetConstraints()); + } + } + InvalidateCaches(static_cast<uint32_t>(-1)); + } +} + std::set<AssetManager2::ApkAssetsPtr> AssetManager2::GetNonSystemOverlays() const { std::set<ApkAssetsPtr> non_system_overlays; for (const PackageGroup& package_group : package_groups_) { @@ -475,6 +500,8 @@ std::set<AssetManager2::ApkAssetsPtr> AssetManager2::GetNonSystemOverlays() cons if (!found_system_package) { auto op = StartOperation(); + // Return all overlays, including the disabled ones as this is used for static info + // collection only. for (const ConfiguredOverlay& overlay : package_group.overlays_) { if (const auto& asset = GetApkAssets(overlay.cookie)) { non_system_overlays.insert(std::move(asset)); @@ -651,7 +678,6 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( auto op = StartOperation(); - // Retrieve the package group from the package id of the resource id. if (UNLIKELY(!is_valid_resid(resid))) { LOG(ERROR) << base::StringPrintf("Invalid resource ID 0x%08x.", resid); @@ -672,7 +698,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( std::optional<FindEntryResult> final_result; bool final_has_locale = false; bool final_overlaid = false; - for (auto & config : configurations_) { + for (auto& config : configurations_) { // Might use this if density_override != 0. ResTable_config density_override_config; @@ -698,7 +724,8 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( } if (!assets->IsLoader()) { for (const auto& id_map : package_group.overlays_) { - auto overlay_entry = id_map.overlay_res_maps_.Lookup(resid); + auto overlay_entry = id_map.enabled ? + id_map.overlay_res_maps_.Lookup(resid) : IdmapResMap::Result(); if (!overlay_entry) { // No id map entry exists for this target resource. continue; @@ -708,7 +735,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( ConfigDescription best_frro_config; Res_value best_frro_value; bool frro_found = false; - for( const auto& [config, value] : overlay_entry.GetInlineValue()) { + for (const auto& [config, value] : overlay_entry.GetInlineValue()) { if ((!frro_found || config.isBetterThan(best_frro_config, desired_config)) && config.match(*desired_config)) { frro_found = true; @@ -1011,7 +1038,7 @@ std::string AssetManager2::GetLastResourceResolution() const { resid, resource_name_string.c_str(), conf.toString().c_str()); char str[40]; str[0] = '\0'; - for(auto iter = configurations_.begin(); iter < configurations_.end(); iter++) { + for (auto iter = configurations_.begin(); iter < configurations_.end(); iter++) { iter->getBcp47Locale(str); log_stream << base::StringPrintf(" %s%s", str, iter < configurations_.end() ? "," : ""); } @@ -1504,7 +1531,7 @@ void AssetManager2::RebuildFilterList() { package.loaded_package_->ForEachTypeSpec([&](const TypeSpec& type_spec, uint8_t type_id) { FilteredConfigGroup* group = nullptr; for (const auto& type_entry : type_spec.type_entries) { - for (auto & config : configurations_) { + for (auto& config : configurations_) { if (type_entry.config.match(config)) { if (!group) { group = &package.filtered_configs_.editItemAt(type_id - 1); @@ -1521,6 +1548,27 @@ void AssetManager2::RebuildFilterList() { } } +bool AssetManager2::IsAnyOverlayConstraintSatisfied(const Idmap_constraints& constraints) const { + if (constraints.constraint_count == 0) { + // There are no constraints, return true. + return true; + } + + for (uint32_t i = 0; i < constraints.constraint_count; i++) { + auto constraint = constraints.constraint_entries[i]; + if (constraint.constraint_type == kOverlayConstraintTypeDisplayId && + constraint.constraint_value == display_id_) { + return true; + } + if (constraint.constraint_type == kOverlayConstraintTypeDeviceId && + constraint.constraint_value == device_id_) { + return true; + } + } + + return false; +} + void AssetManager2::InvalidateCaches(uint32_t diff) { cached_resolved_values_.clear(); diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp index f0ef97e5bdcc..8d1de1af56d2 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -56,13 +56,6 @@ struct Idmap_header { // without having to read/store each header entry separately. }; -struct Idmap_constraint { - // Constraint type can be TYPE_DISPLAY_ID or TYP_DEVICE_ID, please refer - // to ConstraintType in OverlayConstraint.java - uint32_t constraint_type; - uint32_t constraint_value; -}; - struct Idmap_data_header { uint32_t target_entry_count; uint32_t target_inline_entry_count; @@ -148,12 +141,13 @@ status_t OverlayDynamicRefTable::lookupResourceIdNoRewrite(uint32_t* resId) cons return DynamicRefTable::lookupResourceId(resId); } -IdmapResMap::IdmapResMap(const Idmap_data_header* data_header, Idmap_target_entries entries, - Idmap_target_inline_entries inline_entries, +IdmapResMap::IdmapResMap(const Idmap_data_header* data_header, const Idmap_constraints& constraints, + Idmap_target_entries entries, Idmap_target_inline_entries inline_entries, const Idmap_target_entry_inline_value* inline_entry_values, const ConfigDescription* configs, uint8_t target_assigned_package_id, const OverlayDynamicRefTable* overlay_ref_table) : data_header_(data_header), + constraints_(constraints), entries_(entries), inline_entries_(inline_entries), inline_entry_values_(inline_entry_values), @@ -254,7 +248,7 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size } return std::string_view(data, *len); } -} // namespace +} // namespace // O_PATH is a lightweight way of creating an FD, only exists on Linux #ifndef O_PATH @@ -262,9 +256,7 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size #endif LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* header, - const Idmap_constraint* constraints, - uint32_t constraints_count, - const Idmap_data_header* data_header, + const Idmap_data_header* data_header, const Idmap_constraints& constraints, Idmap_target_entries target_entries, Idmap_target_inline_entries target_inline_entries, const Idmap_target_entry_inline_value* inline_entry_values, @@ -272,9 +264,8 @@ LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* head std::unique_ptr<ResStringPool>&& string_pool, std::string_view overlay_apk_path, std::string_view target_apk_path) : header_(header), - constraints_(constraints), - constraints_count_(constraints_count), data_header_(data_header), + constraints_(constraints), target_entries_(target_entries), target_inline_entries_(target_inline_entries), inline_entry_values_(inline_entry_values), @@ -328,16 +319,20 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie return {}; } - auto constraints_count = ReadType<uint32_t>(&data_ptr, &data_size, "constraints count"); - if (!constraints_count) { + auto constraint_count = ReadType<uint32_t>(&data_ptr, &data_size, "constraint count"); + if (!constraint_count) { + LOG(ERROR) << "idmap doesn't have constraint count"; return {}; } - auto constraints = *constraints_count > 0 ? - ReadType<Idmap_constraint>(&data_ptr, &data_size, "constraints", *constraints_count) + auto constraint_entries = *constraint_count > 0 ? + ReadType<Idmap_constraint>(&data_ptr, &data_size, "constraints", dtohl(*constraint_count)) : nullptr; - if (*constraints_count > 0 && !constraints) { + if (*constraint_count > 0 && !constraint_entries) { + LOG(ERROR) << "no constraint entries in idmap with non-zero constraints"; return {}; } + Idmap_constraints constraints{.constraint_count = *constraint_count, + .constraint_entries = constraint_entries}; // Parse the idmap data blocks. Currently idmap2 can only generate one data block. auto data_header = ReadType<Idmap_data_header>(&data_ptr, &data_size, "data header"); @@ -405,10 +400,9 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie // Can't use make_unique because LoadedIdmap constructor is private. return std::unique_ptr<LoadedIdmap>( - new LoadedIdmap(std::string(idmap_path), header, constraints, *constraints_count, - data_header, target_entries, target_inline_entries, - target_inline_entry_values,configurations, overlay_entries, - std::move(idmap_string_pool),*overlay_path, *target_path)); + new LoadedIdmap(std::string(idmap_path), header, data_header, constraints, target_entries, + target_inline_entries, target_inline_entry_values, configurations, + overlay_entries, std::move(idmap_string_pool), *overlay_path, *target_path)); } UpToDate LoadedIdmap::IsUpToDate() const { diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index 0fdeefa09e26..a47fe6a7f50d 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -171,6 +171,8 @@ class AssetManager2 { default_locale_ = default_locale; } + void SetOverlayConstraints(int32_t display_id, int32_t device_id); + // Returns all configurations for which there are resources defined, or an I/O error if reading // resource data failed. // @@ -389,6 +391,9 @@ class AssetManager2 { // The cookie of the overlay assets. ApkAssetsCookie cookie; + + // Enable/disable status of the overlay based on current constraints of AssetManager. + bool enabled; }; // Represents a logical package, which can be made up of many individual packages. Each package @@ -457,6 +462,8 @@ class AssetManager2 { // promoted apk assets when the last operation ends. void FinishOperation() const; + bool IsAnyOverlayConstraintSatisfied(const Idmap_constraints& constraints) const; + // The ordered list of ApkAssets to search. These are not owned by the AssetManager, and must // have a longer lifetime. // The second pair element is the promoted version of the assets, that is held for the duration @@ -480,6 +487,9 @@ class AssetManager2 { // may need to be purged. ftl::SmallVector<ResTable_config, 1> configurations_; + int32_t display_id_; + int32_t device_id_; + // Cached set of bags. These are cached because they can inherit keys from parent bags, // which involves some calculation. mutable std::unordered_map<uint32_t, util::unique_cptr<ResolvedBag>> cached_bags_; diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index 0c0856315d8f..939b62462560 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -59,13 +59,25 @@ inline UpToDate fromBool(bool value) { class LoadedIdmap; class IdmapResMap; struct Idmap_header; -struct Idmap_constraint; +struct Idmap_constraints; struct Idmap_data_header; -struct Idmap_target_entry; struct Idmap_target_entry_inline; struct Idmap_target_entry_inline_value; -struct Idmap_overlay_entry; +// LINT.IfChange +constexpr int32_t kOverlayConstraintTypeDisplayId = 0; +constexpr int32_t kOverlayConstraintTypeDeviceId = 1; +// LINT.ThenChange(../../../../core/java/android/content/om/OverlayConstraint.java) + +struct Idmap_constraint { + // Constraint type can be kOverlayConstraintTypeDisplayId or kOverlayConstraintTypeDeviceId + const uint32_t constraint_type; + const uint32_t constraint_value; +}; +struct Idmap_constraints { + const uint32_t constraint_count = 0; + const Idmap_constraint* constraint_entries = nullptr; +}; struct Idmap_target_entries { const uint32_t* target_id = nullptr; const uint32_t* overlay_id = nullptr; @@ -169,14 +181,19 @@ class IdmapResMap { return overlay_ref_table_; } + inline Idmap_constraints GetConstraints() const { + return constraints_; + } + private: - explicit IdmapResMap(const Idmap_data_header* data_header, Idmap_target_entries entries, - Idmap_target_inline_entries inline_entries, + explicit IdmapResMap(const Idmap_data_header* data_header, const Idmap_constraints& constraints, + Idmap_target_entries entries, Idmap_target_inline_entries inline_entries, const Idmap_target_entry_inline_value* inline_entry_values, const ConfigDescription* configs, uint8_t target_assigned_package_id, const OverlayDynamicRefTable* overlay_ref_table); const Idmap_data_header* data_header_; + Idmap_constraints constraints_; Idmap_target_entries entries_; Idmap_target_inline_entries inline_entries_; const Idmap_target_entry_inline_value* inline_entry_values_; @@ -210,8 +227,9 @@ class LoadedIdmap { // Returns a mapping from target resource ids to overlay values. IdmapResMap GetTargetResourcesMap(uint8_t target_assigned_package_id, const OverlayDynamicRefTable* overlay_ref_table) const { - return IdmapResMap(data_header_, target_entries_, target_inline_entries_, inline_entry_values_, - configurations_, target_assigned_package_id, overlay_ref_table); + return IdmapResMap(data_header_, constraints_, target_entries_, target_inline_entries_, + inline_entry_values_, configurations_, target_assigned_package_id, + overlay_ref_table); } // Returns a dynamic reference table for a loaded overlay package. @@ -223,14 +241,17 @@ class LoadedIdmap { // LoadedIdmap. UpToDate IsUpToDate() const; + inline const Idmap_constraints GetConstraints() const { + return constraints_; + } + protected: // Exposed as protected so that tests can subclass and mock this class out. LoadedIdmap() = default; const Idmap_header* header_; - const Idmap_constraint* constraints_; - uint32_t constraints_count_; const Idmap_data_header* data_header_; + Idmap_constraints constraints_; Idmap_target_entries target_entries_; Idmap_target_inline_entries target_inline_entries_; const Idmap_target_entry_inline_value* inline_entry_values_; @@ -247,9 +268,7 @@ class LoadedIdmap { DISALLOW_COPY_AND_ASSIGN(LoadedIdmap); explicit LoadedIdmap(const std::string& idmap_path, const Idmap_header* header, - const Idmap_constraint* constraints, - uint32_t constraints_count, - const Idmap_data_header* data_header, + const Idmap_data_header* data_header, const Idmap_constraints& constraints, Idmap_target_entries target_entries, Idmap_target_inline_entries target_inline_entries, const Idmap_target_entry_inline_value* inline_entry_values_, diff --git a/libs/hostgraphics/include/gui/BufferItemConsumer.h b/libs/hostgraphics/include/gui/BufferItemConsumer.h index c25941151800..5c96c82e061c 100644 --- a/libs/hostgraphics/include/gui/BufferItemConsumer.h +++ b/libs/hostgraphics/include/gui/BufferItemConsumer.h @@ -17,6 +17,8 @@ #ifndef ANDROID_GUI_BUFFERITEMCONSUMER_H #define ANDROID_GUI_BUFFERITEMCONSUMER_H +#include <com_android_graphics_libgui_flags.h> +#include <gui/BufferQueue.h> #include <gui/ConsumerBase.h> #include <gui/IGraphicBufferConsumer.h> #include <utils/RefBase.h> @@ -26,9 +28,22 @@ namespace android { class BufferItemConsumer : public ConsumerBase { public: BufferItemConsumer(const sp<IGraphicBufferConsumer>& consumer, uint64_t consumerUsage, - int bufferCount, bool controlledByApp) + int bufferCount = -1, bool controlledByApp = false) : mConsumer(consumer) {} +#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + BufferItemConsumer(uint64_t consumerUsage, int bufferCount = -1, + bool controlledByApp = false, bool isConsumerSurfaceFlinger = false) { + sp<IGraphicBufferProducer> producer; + BufferQueue::createBufferQueue(&producer, &mConsumer); + mSurface = sp<Surface>::make(producer, controlledByApp); + } + + status_t setConsumerIsProtected(bool isProtected) { + return OK; + } +#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + status_t acquireBuffer(BufferItem* item, nsecs_t presentWhen, bool waitForFence = true) { return mConsumer->acquireBuffer(item, presentWhen, 0); } @@ -71,8 +86,20 @@ public: return OK; } +#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) +// Returns a Surface that can be used as the producer for this consumer. + sp<Surface> getSurface() const { + return mSurface; + } +#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + private: sp<IGraphicBufferConsumer> mConsumer; +#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + // This Surface wraps the IGraphicBufferConsumer created for this + // ConsumerBase. + sp<Surface> mSurface; +#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) }; } // namespace android diff --git a/libs/hostgraphics/include/gui/Surface.h b/libs/hostgraphics/include/gui/Surface.h index 2774f89cb54c..e268ce6ca769 100644 --- a/libs/hostgraphics/include/gui/Surface.h +++ b/libs/hostgraphics/include/gui/Surface.h @@ -34,6 +34,10 @@ public: ANativeWindow::query = hook_query; } + sp<IGraphicBufferProducer> getIGraphicBufferProducer() const { + return mBufferProducer; + } + static bool isValid(const sp<Surface>& surface) { return surface != nullptr; } diff --git a/libs/hostgraphics/include/ui/Fence.h b/libs/hostgraphics/include/ui/Fence.h index 187c3116f61c..3364b8aed605 100644 --- a/libs/hostgraphics/include/ui/Fence.h +++ b/libs/hostgraphics/include/ui/Fence.h @@ -60,6 +60,10 @@ public: return 0; } + int get() const { + return 0; + } + inline Status getStatus() { // The sync_wait call underlying wait() has been measured to be // significantly faster than the sync_fence_info call underlying diff --git a/libs/hwui/FrameInfo.cpp b/libs/hwui/FrameInfo.cpp index 36feabde07eb..0e52f4e177c5 100644 --- a/libs/hwui/FrameInfo.cpp +++ b/libs/hwui/FrameInfo.cpp @@ -52,8 +52,8 @@ const std::array FrameInfoNames{"Flags", static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 24, "Must update value in FrameMetrics.java#FRAME_STATS_COUNT (and here)"); -void FrameInfo::importUiThreadInfo(int64_t* info) { - memcpy(mFrameInfo, info, UI_THREAD_FRAME_INFO_SIZE * sizeof(int64_t)); +void FrameInfo::importUiThreadInfo(const int64_t* info) { + memcpy(mFrameInfo.data(), info, UI_THREAD_FRAME_INFO_SIZE * sizeof(int64_t)); mSkippedFrameReason.reset(); } diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h index 61c30b803b00..b1f479faadff 100644 --- a/libs/hwui/FrameInfo.h +++ b/libs/hwui/FrameInfo.h @@ -84,6 +84,8 @@ enum { }; }; +using FrameInfoBuffer = std::array<int64_t, static_cast<size_t>(FrameInfoIndex::NumIndexes)>; + class UiFrameInfoBuilder { public: static constexpr int64_t INVALID_VSYNC_ID = -1; @@ -132,7 +134,7 @@ private: class FrameInfo { public: - void importUiThreadInfo(int64_t* info); + void importUiThreadInfo(const int64_t* info); void markSyncStart() { set(FrameInfoIndex::SyncStart) = systemTime(SYSTEM_TIME_MONOTONIC); } @@ -152,7 +154,7 @@ public: set(FrameInfoIndex::Flags) |= static_cast<uint64_t>(frameInfoFlag); } - const int64_t* data() const { return mFrameInfo; } + const FrameInfoBuffer& data() const { return mFrameInfo; } inline int64_t operator[](FrameInfoIndex index) const { return get(index); } @@ -201,7 +203,7 @@ public: } private: - int64_t mFrameInfo[static_cast<int>(FrameInfoIndex::NumIndexes)]; + FrameInfoBuffer mFrameInfo; std::optional<SkippedFrameReason> mSkippedFrameReason; }; diff --git a/libs/hwui/FrameMetricsObserver.h b/libs/hwui/FrameMetricsObserver.h index 3ea49518eecd..d267740d15c4 100644 --- a/libs/hwui/FrameMetricsObserver.h +++ b/libs/hwui/FrameMetricsObserver.h @@ -18,12 +18,14 @@ #include <utils/RefBase.h> +#include "FrameInfo.h" + namespace android { namespace uirenderer { class FrameMetricsObserver : public VirtualLightRefBase { public: - virtual void notify(const int64_t* buffer) = 0; + virtual void notify(const FrameInfoBuffer& buffer) = 0; bool waitForPresentTime() const { return mWaitForPresentTime; }; void reportMetricsFrom(uint64_t frameNumber, int32_t surfaceControlId) { diff --git a/libs/hwui/FrameMetricsReporter.cpp b/libs/hwui/FrameMetricsReporter.cpp index ee32ea17bfaf..4ad9c9ad7b31 100644 --- a/libs/hwui/FrameMetricsReporter.cpp +++ b/libs/hwui/FrameMetricsReporter.cpp @@ -16,10 +16,12 @@ #include "FrameMetricsReporter.h" +#include "FrameInfo.h" + namespace android { namespace uirenderer { -void FrameMetricsReporter::reportFrameMetrics(const int64_t* stats, bool hasPresentTime, +void FrameMetricsReporter::reportFrameMetrics(const FrameInfoBuffer& stats, bool hasPresentTime, uint64_t frameNumber, int32_t surfaceControlId) { FatVector<sp<FrameMetricsObserver>, 10> copy; { diff --git a/libs/hwui/FrameMetricsReporter.h b/libs/hwui/FrameMetricsReporter.h index 7e51df7ce6fc..71b3f52fe12e 100644 --- a/libs/hwui/FrameMetricsReporter.h +++ b/libs/hwui/FrameMetricsReporter.h @@ -35,12 +35,12 @@ class FrameMetricsReporter { public: FrameMetricsReporter() {} - void addObserver(FrameMetricsObserver* observer) { + void addObserver(sp<FrameMetricsObserver>&& observer) { std::lock_guard lock(mObserversLock); - mObservers.push_back(observer); + mObservers.push_back(std::move(observer)); } - bool removeObserver(FrameMetricsObserver* observer) { + bool removeObserver(const sp<FrameMetricsObserver>& observer) { std::lock_guard lock(mObserversLock); for (size_t i = 0; i < mObservers.size(); i++) { if (mObservers[i].get() == observer) { @@ -69,7 +69,7 @@ public: * stats of frames that are from "old" surfaces (i.e. with surfaceControlIds older than the one * the observer was attached on) nor those that are from "old" frame numbers. */ - void reportFrameMetrics(const int64_t* stats, bool hasPresentTime, uint64_t frameNumber, + void reportFrameMetrics(const FrameInfoBuffer& stats, bool hasPresentTime, uint64_t frameNumber, int32_t surfaceControlId); private: diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index 99e7740d66d2..cfec24b17cd4 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -915,20 +915,22 @@ static jboolean android_view_ThreadedRenderer_isDrawingEnabled(JNIEnv*, jclass) static void android_view_ThreadedRenderer_addObserver(JNIEnv* env, jclass clazz, jlong proxyPtr, jlong observerPtr) { - HardwareRendererObserver* observer = reinterpret_cast<HardwareRendererObserver*>(observerPtr); + FrameMetricsObserver* rawObserver = reinterpret_cast<FrameMetricsObserver*>(observerPtr); + sp<FrameMetricsObserver> observer = sp<FrameMetricsObserver>::fromExisting(rawObserver); renderthread::RenderProxy* renderProxy = reinterpret_cast<renderthread::RenderProxy*>(proxyPtr); - renderProxy->addFrameMetricsObserver(observer); + renderProxy->addFrameMetricsObserver(std::move(observer)); } static void android_view_ThreadedRenderer_removeObserver(JNIEnv* env, jclass clazz, jlong proxyPtr, jlong observerPtr) { - HardwareRendererObserver* observer = reinterpret_cast<HardwareRendererObserver*>(observerPtr); + FrameMetricsObserver* rawObserver = reinterpret_cast<FrameMetricsObserver*>(observerPtr); + sp<FrameMetricsObserver> observer = sp<FrameMetricsObserver>::fromExisting(rawObserver); renderthread::RenderProxy* renderProxy = reinterpret_cast<renderthread::RenderProxy*>(proxyPtr); - renderProxy->removeFrameMetricsObserver(observer); + renderProxy->removeFrameMetricsObserver(std::move(observer)); } // ---------------------------------------------------------------------------- diff --git a/libs/hwui/jni/android_graphics_HardwareRendererObserver.cpp b/libs/hwui/jni/android_graphics_HardwareRendererObserver.cpp index 6cae5ffa397f..4f383ee063a2 100644 --- a/libs/hwui/jni/android_graphics_HardwareRendererObserver.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRendererObserver.cpp @@ -16,11 +16,12 @@ #include "android_graphics_HardwareRendererObserver.h" +#include <array> + +#include "FrameInfo.h" #include "graphics_jni_helpers.h" #include "nativehelper/jni_macros.h" -#include <array> - namespace android { struct { @@ -65,13 +66,13 @@ bool HardwareRendererObserver::getNextBuffer(JNIEnv* env, jlongArray metrics, in return false; } -void HardwareRendererObserver::notify(const int64_t* stats) { +void HardwareRendererObserver::notify(const uirenderer::FrameInfoBuffer& stats) { if (!mKeepListening) return; FrameMetricsNotification& elem = mRingBuffer[mNextFree]; if (!elem.hasData.load()) { - memcpy(elem.buffer, stats, kBufferSize * sizeof(stats[0])); + memcpy(elem.buffer, stats.data(), kBufferSize * sizeof(stats[0])); elem.dropCount = mDroppedReports; mDroppedReports = 0; diff --git a/libs/hwui/jni/android_graphics_HardwareRendererObserver.h b/libs/hwui/jni/android_graphics_HardwareRendererObserver.h index 5ee3e1669502..cf20ee135363 100644 --- a/libs/hwui/jni/android_graphics_HardwareRendererObserver.h +++ b/libs/hwui/jni/android_graphics_HardwareRendererObserver.h @@ -43,7 +43,7 @@ public: */ bool getNextBuffer(JNIEnv* env, jlongArray metrics, int* dropCount); - void notify(const int64_t* stats) override; + void notify(const uirenderer::FrameInfoBuffer& stats) override; private: static constexpr int kBufferSize = static_cast<int>(uirenderer::FrameInfoIndex::NumIndexes); diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index e3e393c4fdfb..b248c4bc9ade 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -859,7 +859,7 @@ void CanvasContext::reportMetricsWithPresentTime() { } // release lock } -void CanvasContext::addFrameMetricsObserver(FrameMetricsObserver* observer) { +void CanvasContext::addFrameMetricsObserver(sp<FrameMetricsObserver>&& observer) { std::scoped_lock lock(mFrameInfoMutex); if (mFrameMetricsReporter.get() == nullptr) { mFrameMetricsReporter.reset(new FrameMetricsReporter()); @@ -870,10 +870,10 @@ void CanvasContext::addFrameMetricsObserver(FrameMetricsObserver* observer) { // their frame metrics. uint64_t nextFrameNumber = getFrameNumber(); observer->reportMetricsFrom(nextFrameNumber, mSurfaceControlGenerationId); - mFrameMetricsReporter->addObserver(observer); + mFrameMetricsReporter->addObserver(std::move(observer)); } -void CanvasContext::removeFrameMetricsObserver(FrameMetricsObserver* observer) { +void CanvasContext::removeFrameMetricsObserver(const sp<FrameMetricsObserver>& observer) { std::scoped_lock lock(mFrameInfoMutex); if (mFrameMetricsReporter.get() != nullptr) { mFrameMetricsReporter->removeObserver(observer); diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index cb3753822035..f119102dc2a2 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -176,8 +176,8 @@ public: void setContentDrawBounds(const Rect& bounds) { mContentDrawBounds = bounds; } - void addFrameMetricsObserver(FrameMetricsObserver* observer); - void removeFrameMetricsObserver(FrameMetricsObserver* observer); + void addFrameMetricsObserver(sp<FrameMetricsObserver>&& observer); + void removeFrameMetricsObserver(const sp<FrameMetricsObserver>& observer); // Used to queue up work that needs to be completed before this frame completes void enqueueFrameWork(std::function<void()>&& func); diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index 715153b5083d..ebfd8fde91f6 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -206,7 +206,7 @@ void RenderProxy::buildLayer(RenderNode* node) { } bool RenderProxy::copyLayerInto(DeferredLayerUpdater* layer, SkBitmap& bitmap) { - ATRACE_NAME("TextureView#getBitmap"); + ATRACE_NAME("RenderProxy#copyLayerInto readback"); auto& thread = RenderThread::getInstance(); return thread.queue().runSync([&]() -> bool { return thread.readback().copyLayerInto(layer, &bitmap) == CopyResult::Success; @@ -420,15 +420,15 @@ void RenderProxy::setFrameCompleteCallback(std::function<void()>&& callback) { mDrawFrameTask.setFrameCompleteCallback(std::move(callback)); } -void RenderProxy::addFrameMetricsObserver(FrameMetricsObserver* observerPtr) { - mRenderThread.queue().post([this, observer = sp{observerPtr}]() { - mContext->addFrameMetricsObserver(observer.get()); +void RenderProxy::addFrameMetricsObserver(sp<FrameMetricsObserver>&& observer) { + mRenderThread.queue().post([this, observer = std::move(observer)]() mutable { + mContext->addFrameMetricsObserver(std::move(observer)); }); } -void RenderProxy::removeFrameMetricsObserver(FrameMetricsObserver* observerPtr) { - mRenderThread.queue().post([this, observer = sp{observerPtr}]() { - mContext->removeFrameMetricsObserver(observer.get()); +void RenderProxy::removeFrameMetricsObserver(sp<FrameMetricsObserver>&& observer) { + mRenderThread.queue().post([this, observer = std::move(observer)]() { + mContext->removeFrameMetricsObserver(observer); }); } @@ -473,7 +473,7 @@ void RenderProxy::prepareToDraw(Bitmap& bitmap) { } int RenderProxy::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) { - ATRACE_NAME("HardwareBitmap readback"); + ATRACE_NAME("RenderProxy#copyHWBitmapInto readback"); RenderThread& thread = RenderThread::getInstance(); if (RenderThread::isCurrent()) { // TODO: fix everything that hits this. We should never be triggering a readback ourselves. @@ -485,6 +485,7 @@ int RenderProxy::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) { } int RenderProxy::copyImageInto(const sk_sp<SkImage>& image, SkBitmap* bitmap) { + ATRACE_NAME("RenderProxy#copyImageInto readback"); RenderThread& thread = RenderThread::getInstance(); if (RenderThread::isCurrent()) { // TODO: fix everything that hits this. We should never be triggering a readback ourselves. diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h index f2d8e94c7bd2..ad6d54bfcf91 100644 --- a/libs/hwui/renderthread/RenderProxy.h +++ b/libs/hwui/renderthread/RenderProxy.h @@ -141,8 +141,8 @@ public: void setFrameCommitCallback(std::function<void(bool)>&& callback); void setFrameCompleteCallback(std::function<void()>&& callback); - void addFrameMetricsObserver(FrameMetricsObserver* observer); - void removeFrameMetricsObserver(FrameMetricsObserver* observer); + void addFrameMetricsObserver(sp<FrameMetricsObserver>&& observer); + void removeFrameMetricsObserver(sp<FrameMetricsObserver>&& observer); void setForceDark(ForceDarkType type); static void copySurfaceInto(ANativeWindow* window, std::shared_ptr<CopyRequest>&& request); diff --git a/libs/hwui/tests/unit/FrameMetricsReporterTests.cpp b/libs/hwui/tests/unit/FrameMetricsReporterTests.cpp index 571a26707c93..c7935ac5a753 100644 --- a/libs/hwui/tests/unit/FrameMetricsReporterTests.cpp +++ b/libs/hwui/tests/unit/FrameMetricsReporterTests.cpp @@ -14,13 +14,14 @@ * limitations under the License. */ -#include <gmock/gmock.h> -#include <gtest/gtest.h> - #include <FrameMetricsObserver.h> #include <FrameMetricsReporter.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> #include <utils/TimeUtils.h> +#include "FrameInfo.h" + using namespace android; using namespace android::uirenderer; @@ -31,7 +32,7 @@ public: explicit TestFrameMetricsObserver(bool waitForPresentTime) : FrameMetricsObserver(waitForPresentTime){}; - MOCK_METHOD(void, notify, (const int64_t* buffer), (override)); + MOCK_METHOD(void, notify, (const FrameInfoBuffer& buffer), (override)); }; // To make sure it is clear that something went wrong if no from frame is set (to make it easier @@ -44,7 +45,7 @@ TEST(FrameMetricsReporter, doesNotReportAnyFrameIfNoFromFrameIsSpecified) { reporter->addObserver(observer.get()); - const int64_t* stats; + FrameInfoBuffer stats; bool hasPresentTime = false; uint64_t frameNumber = 1; int32_t surfaceControlId = 0; @@ -64,7 +65,7 @@ TEST(FrameMetricsReporter, doesNotReportAnyFrameIfNoFromFrameIsSpecified) { } TEST(FrameMetricsReporter, respectsWaitForPresentTimeUnset) { - const int64_t* stats; + FrameInfoBuffer stats; bool hasPresentTime = false; uint64_t frameNumber = 3; int32_t surfaceControlId = 0; @@ -85,7 +86,7 @@ TEST(FrameMetricsReporter, respectsWaitForPresentTimeUnset) { } TEST(FrameMetricsReporter, respectsWaitForPresentTimeSet) { - const int64_t* stats; + FrameInfoBuffer stats; bool hasPresentTime = true; uint64_t frameNumber = 3; int32_t surfaceControlId = 0; @@ -106,7 +107,7 @@ TEST(FrameMetricsReporter, respectsWaitForPresentTimeSet) { } TEST(FrameMetricsReporter, reportsAllFramesAfterSpecifiedFromFrame) { - const int64_t* stats; + FrameInfoBuffer stats; bool hasPresentTime = false; std::vector<uint64_t> frameNumbers{0, 1, 10}; @@ -138,7 +139,7 @@ TEST(FrameMetricsReporter, reportsAllFramesAfterSpecifiedFromFrame) { } TEST(FrameMetricsReporter, doesNotReportsFramesBeforeSpecifiedFromFrame) { - const int64_t* stats; + FrameInfoBuffer stats; bool hasPresentTime = false; std::vector<uint64_t> frameNumbers{1, 10}; @@ -165,7 +166,7 @@ TEST(FrameMetricsReporter, doesNotReportsFramesBeforeSpecifiedFromFrame) { } TEST(FrameMetricsReporter, canRemoveObservers) { - const int64_t* stats; + FrameInfoBuffer stats; bool hasPresentTime = false; uint64_t frameNumber = 3; int32_t surfaceControlId = 0; @@ -187,7 +188,7 @@ TEST(FrameMetricsReporter, canRemoveObservers) { } TEST(FrameMetricsReporter, canSupportMultipleObservers) { - const int64_t* stats; + FrameInfoBuffer stats; bool hasPresentTime = false; uint64_t frameNumber = 3; int32_t surfaceControlId = 0; diff --git a/libs/hwui/tests/unit/JankTrackerTests.cpp b/libs/hwui/tests/unit/JankTrackerTests.cpp index c289d67fbef6..08718c959150 100644 --- a/libs/hwui/tests/unit/JankTrackerTests.cpp +++ b/libs/hwui/tests/unit/JankTrackerTests.cpp @@ -14,18 +14,19 @@ * limitations under the License. */ -#include <gtest/gtest.h> -#include <gmock/gmock.h> - #include <JankTracker.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> #include <utils/TimeUtils.h> +#include "FrameInfo.h" + using namespace android; using namespace android::uirenderer; class TestFrameMetricsObserver : public FrameMetricsObserver { public: - void notify(const int64_t*) {} + void notify(const FrameInfoBuffer&) override {} }; TEST(JankTracker, noJank) { diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index 88981eac9bb5..1cb540b09fb3 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -481,9 +481,9 @@ public final class MediaRoute2Info implements Parcelable { /** * Indicates that a route supports routing playback to remote routes through control commands. * - * <p>This type of routing does not affect affect this system's audio or video, but instead - * relies on the device that corresponds to this route to fetch and play the media. It also - * requires the media app to take care of initializing and controlling playback. + * <p>This type of routing does not affect this system's audio or video, but instead relies on + * the device that corresponds to this route to fetch and play the media. It also requires the + * media app to take care of initializing and controlling playback. */ @FlaggedApi(FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) public static final int FLAG_ROUTING_TYPE_REMOTE = 1 << 2; diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java index e94fb7d9e52b..4ae8daa63e1d 100644 --- a/media/java/android/media/MediaRoute2ProviderService.java +++ b/media/java/android/media/MediaRoute2ProviderService.java @@ -348,18 +348,21 @@ public abstract class MediaRoute2ProviderService extends Service { * <p>This method must only be called as the result of a prior call to {@link * #onCreateSystemRoutingSession}. * + * <p>This method returns a {@link MediaStreams} instance that holds the media streams to route + * as part of the newly created routing session. May be null if system media capture failed, in + * which case you can ignore the return value, as you will receive a call to {@link + * #onReleaseSession} where you can clean up this session. {@link AudioRecord#startRecording()} + * must be called immediately on {@link MediaStreams#getAudioRecord()} after calling this + * method, in order to start streaming audio to the receiver. + * * @param requestId the ID of the {@link #onCreateSystemRoutingSession} request which this call * is in response to. * @param sessionInfo a {@link RoutingSessionInfo} that describes the newly created routing * session. * @param formats the {@link MediaStreamsFormats} that describes the format for the {@link * MediaStreams} to return. - * @return a {@link MediaStreams} instance that holds the media streams to route as part of the - * newly created routing session. May be null if system media capture failed, in which case - * you can ignore the return value, as you will receive a call to {@link #onReleaseSession} - * where you can clean up this session. {@link AudioRecord#startRecording()} must be called - * immediately on {@link MediaStreams#getAudioRecord()} after calling this method, in order - * to start streaming audio to the receiver. + * @return The {@link MediaStreams} to route as part of the new session, or null if system media + * capture failed and the result can be ignored. * @throws IllegalStateException If the provided {@code requestId} doesn't correspond to a * previous call to {@link #onCreateSystemRoutingSession}. */ @@ -1191,8 +1194,8 @@ public abstract class MediaRoute2ProviderService extends Service { * * <p>The default value is an empty {@link Bundle}. * - * <p>Note that this bundle is not copied, so avoiding mutating the given {@link Bundle} - * after passing it to this method. + * <p>Do not mutate the given {@link Bundle} after passing it to this method. You can + * use {@link Bundle#deepCopy()} to keep a mutable copy. */ @NonNull public Builder setExtras(@NonNull Bundle extras) { diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp index 4fe0b80f3951..275972495206 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -715,47 +715,45 @@ void ASurfaceTransaction_setLuts(ASurfaceTransaction* aSurfaceTransaction, sp<SurfaceControl> surfaceControl = ASurfaceControl_to_SurfaceControl(aSurfaceControl); Transaction* transaction = ASurfaceTransaction_to_Transaction(aSurfaceTransaction); - int fd = -1; + base::unique_fd fd; std::vector<int32_t> offsets; std::vector<int32_t> dimensions; std::vector<int32_t> sizes; std::vector<int32_t> samplingKeys; if (luts) { - std::vector<float> buffer(luts->totalBufferSize); int32_t count = luts->offsets.size(); offsets = luts->offsets; dimensions.reserve(count); sizes.reserve(count); samplingKeys.reserve(count); - for (int32_t i = 0; i < count; i++) { - dimensions.emplace_back(luts->entries[i]->properties.dimension); - sizes.emplace_back(luts->entries[i]->properties.size); - samplingKeys.emplace_back(luts->entries[i]->properties.samplingKey); - std::copy(luts->entries[i]->buffer.data.begin(), luts->entries[i]->buffer.data.end(), - buffer.begin() + offsets[i]); - } // mmap - fd = ashmem_create_region("lut_shared_mem", luts->totalBufferSize * sizeof(float)); + fd.reset(ashmem_create_region("lut_shared_mem", luts->totalBufferSize * sizeof(float))); if (fd < 0) { LOG_ALWAYS_FATAL("setLuts, ashmem_create_region() failed"); return; } - void* ptr = mmap(nullptr, luts->totalBufferSize * sizeof(float), PROT_READ | PROT_WRITE, - MAP_SHARED, fd, 0); + float* ptr = (float*)mmap(nullptr, luts->totalBufferSize * sizeof(float), + PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (ptr == MAP_FAILED) { LOG_ALWAYS_FATAL("setLuts, Failed to map the shared memory"); return; } - memcpy(ptr, buffer.data(), luts->totalBufferSize * sizeof(float)); + for (int32_t i = 0; i < count; i++) { + dimensions.emplace_back(luts->entries[i]->properties.dimension); + sizes.emplace_back(luts->entries[i]->properties.size); + samplingKeys.emplace_back(luts->entries[i]->properties.samplingKey); + std::copy(luts->entries[i]->buffer.data.begin(), luts->entries[i]->buffer.data.end(), + ptr + offsets[i]); + } + munmap(ptr, luts->totalBufferSize * sizeof(float)); } - transaction->setLuts(surfaceControl, base::unique_fd(fd), offsets, dimensions, sizes, - samplingKeys); + transaction->setLuts(surfaceControl, std::move(fd), offsets, dimensions, sizes, samplingKeys); } void ASurfaceTransaction_setColor(ASurfaceTransaction* aSurfaceTransaction, diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt index d6e19a6193fd..a75aeaff0c48 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt @@ -104,6 +104,7 @@ fun WearApp( scrollable(Screen.MultipleCredentialsScreenFlatten.route) { MultiCredentialsFlattenScreen( credentialSelectorUiState = (remember { uiState } as MultipleEntry), + columnState = it.columnState, flowEngine = flowEngine, ) } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt index 932b3456bfb8..96cadab3916a 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt @@ -36,7 +36,7 @@ import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.ui.components.CredentialsScreenChipSpacer import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.layout.ScalingLazyColumn -import com.google.android.horologist.compose.layout.rememberColumnState +import com.google.android.horologist.compose.layout.ScalingLazyColumnState import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults import androidx.compose.ui.text.style.TextAlign import androidx.wear.compose.material.MaterialTheme as WearMaterialTheme @@ -45,6 +45,7 @@ import androidx.wear.compose.material.MaterialTheme as WearMaterialTheme * Screen that shows multiple credentials to select from, grouped by accounts * * @param credentialSelectorUiState The app bar view model. + * @param columnState ScalingLazyColumn configuration to be be applied * @param modifier styling for composable * @param flowEngine [FlowEngine] that updates ui state for this screen */ @@ -52,15 +53,14 @@ import androidx.wear.compose.material.MaterialTheme as WearMaterialTheme @Composable fun MultiCredentialsFlattenScreen( credentialSelectorUiState: MultipleEntry, + columnState: ScalingLazyColumnState, flowEngine: FlowEngine, ) { val selectEntry = flowEngine.getEntrySelector() Row { Spacer(Modifier.weight(0.052f)) // 5.2% side margin ScalingLazyColumn( - columnState = rememberColumnState( - ScalingLazyColumnDefaults.belowTimeText(horizontalAlignment = Alignment.Start), - ), + columnState = columnState, modifier = Modifier.weight(0.896f).fillMaxSize(), // 5.2% side margin ) { diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp index bd892637a5eb..853d1ad0dc94 100644 --- a/packages/CtsShim/build/Android.bp +++ b/packages/CtsShim/build/Android.bp @@ -152,34 +152,6 @@ android_app { } //########################################################## -// Variant: System app upgrade - -android_app { - name: "CtsShimUpgrade", - - sdk_version: "current", - optimize: { - enabled: false, - }, - dex_preopt: { - enabled: false, - }, - - manifest: "shim/AndroidManifestUpgrade.xml", - min_sdk_version: "24", -} - -java_genrule { - name: "generate_shim_manifest", - srcs: [ - "shim/AndroidManifest.xml", - ":CtsShimUpgrade", - ], - out: ["AndroidManifest.xml"], - cmd: "sed -e s/__HASH__/`sha512sum -b $(location :CtsShimUpgrade) | cut -d' ' -f1`/ $(location shim/AndroidManifest.xml) > $(out)", -} - -//########################################################## // Variant: System app android_app { @@ -193,7 +165,7 @@ android_app { enabled: false, }, - manifest: ":generate_shim_manifest", + manifest: "shim/AndroidManifest.xml", apex_available: [ "//apex_available:platform", "com.android.apex.cts.shim.v1", diff --git a/packages/CtsShim/build/shim/AndroidManifest.xml b/packages/CtsShim/build/shim/AndroidManifest.xml index 3b8276b66bac..1ffe56c0c76a 100644 --- a/packages/CtsShim/build/shim/AndroidManifest.xml +++ b/packages/CtsShim/build/shim/AndroidManifest.xml @@ -17,15 +17,13 @@ <!-- Manifest for the system CTS shim --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - package="com.android.cts.ctsshim" - android:sharedUserId="com.android.cts.ctsshim" > + package="com.android.cts.ctsshim" > - <uses-sdk - android:minSdkVersion="24" + <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="28" /> <restrict-update - android:hash="__HASH__" /> + android:hash="__CAN_NOT_BE_UPDATED__" /> <application android:hasCode="false" diff --git a/packages/CtsShim/build/shim/AndroidManifestUpgrade.xml b/packages/CtsShim/build/shim/AndroidManifestUpgrade.xml deleted file mode 100644 index 7f3644a18ad6..000000000000 --- a/packages/CtsShim/build/shim/AndroidManifestUpgrade.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 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 for the system CTS shim --> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - package="com.android.cts.ctsshim" > - - <uses-sdk - android:minSdkVersion="24" - android:targetSdkVersion="28" /> - - <application - android:hasCode="false" - tools:ignore="AllowBackup,MissingApplicationIcon" /> -</manifest> - diff --git a/packages/NeuralNetworks/OWNERS b/packages/NeuralNetworks/OWNERS index 6b391503b5c4..bf3c8fe1550f 100644 --- a/packages/NeuralNetworks/OWNERS +++ b/packages/NeuralNetworks/OWNERS @@ -1,5 +1,4 @@ # Bug component: 195575 sandeepbandaru@google.com -shivanker@google.com -shiqing@google.com
\ No newline at end of file +shiqing@google.com diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp index a30c0c3c6d4c..8b1828c5f41f 100644 --- a/packages/PackageInstaller/Android.bp +++ b/packages/PackageInstaller/Android.bp @@ -47,19 +47,21 @@ android_app { sdk_version: "system_current", rename_resources_package: false, static_libs: [ - "androidx.leanback_leanback", + "android.content.pm.flags-aconfig-java", + "android.multiuser.flags-aconfig-java", + "android.os.flags-aconfig-java", "androidx.annotation_annotation", "androidx.fragment_fragment", - "androidx.lifecycle_lifecycle-livedata", + "androidx.leanback_leanback", "androidx.lifecycle_lifecycle-extensions", - "android.content.pm.flags-aconfig-java", - "android.os.flags-aconfig-java", - "android.multiuser.flags-aconfig-java", + "androidx.lifecycle_lifecycle-livedata", + "kotlin-parcelize-runtime", ], lint: { error_checks: ["Recycle"], }, + kotlin_plugins: ["kotlin-parcelize-compiler-plugin"], } android_app { @@ -79,19 +81,22 @@ android_app { overrides: ["PackageInstaller"], static_libs: [ - "androidx.leanback_leanback", - "androidx.fragment_fragment", - "androidx.lifecycle_lifecycle-livedata", - "androidx.lifecycle_lifecycle-extensions", "android.content.pm.flags-aconfig-java", - "android.os.flags-aconfig-java", "android.multiuser.flags-aconfig-java", + "android.os.flags-aconfig-java", + "androidx.annotation_annotation", + "androidx.fragment_fragment", + "androidx.leanback_leanback", + "androidx.lifecycle_lifecycle-extensions", + "androidx.lifecycle_lifecycle-livedata", + "kotlin-parcelize-runtime", ], aaptflags: ["--product tablet"], lint: { error_checks: ["Recycle"], }, + kotlin_plugins: ["kotlin-parcelize-compiler-plugin"], } android_app { @@ -111,18 +116,20 @@ android_app { overrides: ["PackageInstaller"], static_libs: [ - "androidx.leanback_leanback", + "android.content.pm.flags-aconfig-java", + "android.multiuser.flags-aconfig-java", + "android.os.flags-aconfig-java", "androidx.annotation_annotation", "androidx.fragment_fragment", - "androidx.lifecycle_lifecycle-livedata", + "androidx.leanback_leanback", "androidx.lifecycle_lifecycle-extensions", - "android.content.pm.flags-aconfig-java", - "android.os.flags-aconfig-java", - "android.multiuser.flags-aconfig-java", + "androidx.lifecycle_lifecycle-livedata", + "kotlin-parcelize-runtime", ], aaptflags: ["--product tv"], lint: { error_checks: ["Recycle"], }, + kotlin_plugins: ["kotlin-parcelize-compiler-plugin"], } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt index 8de8fbb3e688..a8dad096e4b0 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt @@ -18,8 +18,8 @@ package com.android.packageinstaller.v2.model import android.app.Activity import android.content.Intent -import android.content.pm.PackageManager import android.content.pm.PackageInstaller +import android.content.pm.PackageManager import android.graphics.drawable.Drawable sealed class InstallStage(val stageCode: Int) { @@ -42,7 +42,7 @@ class InstallReady : InstallStage(STAGE_READY) data class InstallUserActionRequired( val actionReason: Int, - private val appSnippet: PackageUtil.AppSnippet? = null, + val appSnippet: PackageUtil.AppSnippet? = null, val isAppUpdating: Boolean = false, /** * This holds either a package name or the app label of the install source. @@ -63,7 +63,7 @@ data class InstallUserActionRequired( } } -data class InstallInstalling(private val appSnippet: PackageUtil.AppSnippet) : +data class InstallInstalling(val appSnippet: PackageUtil.AppSnippet) : InstallStage(STAGE_INSTALLING) { val appIcon: Drawable? @@ -74,7 +74,7 @@ data class InstallInstalling(private val appSnippet: PackageUtil.AppSnippet) : } data class InstallSuccess( - private val appSnippet: PackageUtil.AppSnippet, + val appSnippet: PackageUtil.AppSnippet, val shouldReturnResult: Boolean = false, /** * @@ -95,7 +95,7 @@ data class InstallSuccess( } data class InstallFailed( - private val appSnippet: PackageUtil.AppSnippet? = null, + val appSnippet: PackageUtil.AppSnippet? = null, val legacyCode: Int, val statusCode: Int, val message: String? = null, 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 828a95fcbb01..e8477ef261a8 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt @@ -17,21 +17,32 @@ package com.android.packageinstaller.v2.model import android.Manifest +import android.annotation.SuppressLint +import android.app.ActivityManager import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build +import android.os.Parcel +import android.os.Parcelable import android.os.Process import android.os.UserHandle import android.os.UserManager import android.util.Log +import com.android.packageinstaller.v2.model.PackageUtil.getAppSnippet +import java.io.ByteArrayOutputStream import java.io.File +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize object PackageUtil { private val LOG_TAG = InstallRepository::class.java.simpleName @@ -39,6 +50,24 @@ object PackageUtil { private const val SPLIT_BASE_APK_SUFFIX = "base.apk" const val localLogv = false + const val ARGS_ABORT_REASON: String = "abort_reason" + const val ARGS_ACTION_REASON: String = "action_reason" + const val ARGS_ACTIVITY_RESULT_CODE: String = "activity_result_code" + const val ARGS_APP_DATA_SIZE: String = "app_data_size" + const val ARGS_APP_LABEL: String = "app_label" + const val ARGS_APP_SNIPPET: String = "app_snippet" + const val ARGS_ERROR_DIALOG_TYPE: String = "error_dialog_type" + const val ARGS_IS_ARCHIVE: String = "is_archive" + const val ARGS_IS_CLONE_USER: String = "clone_user" + const val ARGS_IS_UPDATING: String = "is_updating" + const val ARGS_LEGACY_CODE: String = "legacy_code" + const val ARGS_MESSAGE: String = "message" + const val ARGS_RESULT_INTENT: String = "result_intent" + const val ARGS_SHOULD_RETURN_RESULT: String = "should_return_result" + const val ARGS_SOURCE_APP: String = "source_app" + const val ARGS_STATUS_CODE: String = "status_code" + const val ARGS_TITLE: String = "title" + /** * Determines if the UID belongs to the system downloads provider and returns the * [ApplicationInfo] of the provider @@ -238,7 +267,8 @@ object PackageUtil { context.resources, info.getAppIcon() ) else pm.defaultActivityIcon - return AppSnippet(label, icon) + val largeIconSize = getLargeIconSize(context) + return AppSnippet(label, icon, largeIconSize) } /** @@ -247,8 +277,11 @@ object PackageUtil { */ @JvmStatic fun getAppSnippet(context: Context, pkgInfo: PackageInfo): AppSnippet { + val largeIconSize = getLargeIconSize(context) return pkgInfo.applicationInfo?.let { getAppSnippet(context, it) } ?: run { - AppSnippet(pkgInfo.packageName, context.packageManager.defaultActivityIcon) + AppSnippet( + pkgInfo.packageName, context.packageManager.defaultActivityIcon, largeIconSize + ) } } @@ -261,7 +294,8 @@ object PackageUtil { val pm = context.packageManager val label = pm.getApplicationLabel(appInfo) val icon = pm.getApplicationIcon(appInfo) - return AppSnippet(label, icon) + val largeIconSize = getLargeIconSize(context) + return AppSnippet(label, icon, largeIconSize) } /** @@ -270,16 +304,24 @@ object PackageUtil { */ @JvmStatic fun getAppSnippet(context: Context, pkgInfo: PackageInfo, sourceFile: File): AppSnippet { + val largeIconSize = getLargeIconSize(context) pkgInfo.applicationInfo?.let { val appInfoFromFile = processAppInfoForFile(it, sourceFile) val label = getAppLabelFromFile(context, appInfoFromFile) val icon = getAppIconFromFile(context, appInfoFromFile) - return AppSnippet(label, icon) + return AppSnippet(label, icon, largeIconSize) } ?: run { - return AppSnippet(pkgInfo.packageName, context.packageManager.defaultActivityIcon) + return AppSnippet( + pkgInfo.packageName, context.packageManager.defaultActivityIcon, largeIconSize + ) } } + private fun getLargeIconSize(context: Context): Int { + val am = context.getSystemService<ActivityManager>(ActivityManager::class.java) + return am.launcherLargeIconSize + } + /** * Utility method to load application label * @@ -438,7 +480,69 @@ object PackageUtil { * The class to hold an incoming package's icon and label. * See [getAppSnippet] */ - data class AppSnippet(var label: CharSequence?, var icon: Drawable?) { + @Parcelize + data class AppSnippet( + var label: CharSequence?, + var icon: Drawable?, + var iconSize: Int, + ) : Parcelable { + private companion object : Parceler<AppSnippet> { + override fun AppSnippet.write(dest: Parcel, flags: Int) { + dest.writeString(label.toString()) + + val bmp = getBitmapFromDrawable(icon!!) + dest.writeBlob(getBytesFromBitmap(bmp)) + bmp.recycle() + + dest.writeInt(iconSize) + } + + @SuppressLint("UseKtx") + override fun create(parcel: Parcel): AppSnippet { + val label = parcel.readString() + + val b: ByteArray = parcel.readBlob()!! + val bmp: Bitmap? = BitmapFactory.decodeByteArray(b, 0, b.size) + val icon = BitmapDrawable(Resources.getSystem(), bmp) + + val iconSize = parcel.readInt() + + return AppSnippet(label.toString(), icon, iconSize) + } + } + + @SuppressLint("UseKtx") + private fun getBitmapFromDrawable(drawable: Drawable): Bitmap { + // Create an empty bitmap with the dimensions of our drawable + val bmp = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, Bitmap.Config.ARGB_8888 + ) + // Associate it with a canvas. This canvas will draw the icon on the bitmap + val canvas = Canvas(bmp) + // Draw the drawable in the canvas. The canvas will ultimately paint the drawable in the + // bitmap held within + drawable.draw(canvas) + + // Scale it down if the icon is too large + if ((bmp.getWidth() > iconSize * 2) || (bmp.getHeight() > iconSize * 2)) { + val scaledBitmap = Bitmap.createScaledBitmap(bmp, iconSize, iconSize, true) + if (scaledBitmap != bmp) { + bmp.recycle() + } + return scaledBitmap + } + return bmp + } + + private fun getBytesFromBitmap(bmp: Bitmap): ByteArray? { + var baos = ByteArrayOutputStream() + baos.use { + bmp.compress(Bitmap.CompressFormat.PNG, 100, it) + } + return baos.toByteArray() + } + override fun toString(): String { return "AppSnippet[label = $label, hasIcon = ${icon != null}]" } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt index 481023ed5677..4a8be8db9248 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt @@ -133,9 +133,10 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { val aborted = installStage as InstallAborted when (aborted.abortReason) { InstallAborted.ABORT_REASON_DONE, - InstallAborted.ABORT_REASON_INTERNAL_ERROR -> { + InstallAborted.ABORT_REASON_INTERNAL_ERROR, + -> { if (aborted.errorDialogType == InstallAborted.DLG_PACKAGE_ERROR) { - val parseErrorDialog = ParseErrorFragment(aborted) + val parseErrorDialog = ParseErrorFragment.newInstance(aborted) showDialogInner(parseErrorDialog) } else { setResult(aborted.activityResultCode, aborted.resultIntent, true) @@ -151,12 +152,12 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { val uar = installStage as InstallUserActionRequired when (uar.actionReason) { InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION -> { - val actionDialog = InstallConfirmationFragment(uar) + val actionDialog = InstallConfirmationFragment.newInstance(uar) showDialogInner(actionDialog) } InstallUserActionRequired.USER_ACTION_REASON_UNKNOWN_SOURCE -> { - val externalSourceDialog = ExternalSourcesBlockedFragment(uar) + val externalSourceDialog = ExternalSourcesBlockedFragment.newInstance(uar) showDialogInner(externalSourceDialog) } @@ -169,7 +170,7 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { InstallStage.STAGE_INSTALLING -> { val installing = installStage as InstallInstalling - val installingDialog = InstallInstallingFragment(installing) + val installingDialog = InstallInstallingFragment.newInstance(installing) showDialogInner(installingDialog) } @@ -179,7 +180,7 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { val successIntent = success.resultIntent setResult(RESULT_OK, successIntent, true) } else { - val successDialog = InstallSuccessFragment(success) + val successDialog = InstallSuccessFragment.newInstance(success) showDialogInner(successDialog) } } @@ -190,7 +191,7 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { val failureIntent = failed.resultIntent setResult(RESULT_FIRST_USER, failureIntent, true) } else { - val failureDialog = InstallFailedFragment(failed) + val failureDialog = InstallFailedFragment.newInstance(failed) showDialogInner(failureDialog) } } @@ -242,11 +243,11 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { } return when (restriction) { UserManager.DISALLOW_INSTALL_APPS -> - SimpleErrorFragment(R.string.install_apps_user_restriction_dlg_text) + SimpleErrorFragment.newInstance(R.string.install_apps_user_restriction_dlg_text) UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY -> - SimpleErrorFragment(R.string.unknown_apps_user_restriction_dlg_text) + SimpleErrorFragment.newInstance(R.string.unknown_apps_user_restriction_dlg_text) else -> null } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt index 0a02845e0dd3..08bc7666c3e1 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt @@ -101,7 +101,7 @@ class UninstallLaunch : FragmentActivity(), UninstallActionListener { if (aborted.abortReason == UninstallAborted.ABORT_REASON_APP_UNAVAILABLE || aborted.abortReason == UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED ) { - val errorDialog = UninstallErrorFragment(aborted) + val errorDialog = UninstallErrorFragment.newInstance(aborted) showDialogInner(errorDialog) } else { setResult(aborted.activityResultCode, null, true) @@ -110,7 +110,7 @@ class UninstallLaunch : FragmentActivity(), UninstallActionListener { UninstallStage.STAGE_USER_ACTION_REQUIRED -> { val uar = uninstallStage as UninstallUserActionRequired - val confirmationDialog = UninstallConfirmationFragment(uar) + val confirmationDialog = UninstallConfirmationFragment.newInstance(uar) showDialogInner(confirmationDialog) } @@ -120,7 +120,7 @@ class UninstallLaunch : FragmentActivity(), UninstallActionListener { // And a fragment if the user requests a result back. Should we consolidate and // show a fragment always? val uninstalling = uninstallStage as UninstallUninstalling - val uninstallingDialog = UninstallUninstallingFragment(uninstalling) + val uninstallingDialog = UninstallUninstallingFragment.newInstance(uninstalling) showDialogInner(uninstallingDialog) } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java index 343a213780b3..4c69b9d20315 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java @@ -16,17 +16,25 @@ package com.android.packageinstaller.v2.ui.fragments; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_ACTION_REASON; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_APP_SNIPPET; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_IS_UPDATING; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_SOURCE_APP; + import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; + import com.android.packageinstaller.R; import com.android.packageinstaller.v2.model.InstallUserActionRequired; +import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet; import com.android.packageinstaller.v2.ui.InstallActionListener; /** @@ -37,14 +45,34 @@ public class ExternalSourcesBlockedFragment extends DialogFragment { private static final String LOG_TAG = ExternalSourcesBlockedFragment.class.getSimpleName(); @NonNull - private final InstallUserActionRequired mDialogData; + private InstallUserActionRequired mDialogData; @NonNull private InstallActionListener mInstallActionListener; @NonNull private AlertDialog mDialog; - public ExternalSourcesBlockedFragment(InstallUserActionRequired dialogData) { - mDialogData = dialogData; + public ExternalSourcesBlockedFragment() { + // Required for DialogFragment + } + + /** + * Creates a new instance of this fragment with necessary data set as fragment arguments + * + * @param dialogData {@link InstallUserActionRequired} object containing data to display + * in the dialog + * @return an instance of the fragment + */ + public static ExternalSourcesBlockedFragment newInstance( + @NonNull InstallUserActionRequired dialogData) { + Bundle args = new Bundle(); + args.putInt(ARGS_ACTION_REASON, dialogData.getActionReason()); + args.putParcelable(ARGS_APP_SNIPPET, dialogData.getAppSnippet()); + args.putBoolean(ARGS_IS_UPDATING, dialogData.isAppUpdating()); + args.putString(ARGS_SOURCE_APP, dialogData.getSourceApp()); + + ExternalSourcesBlockedFragment fragment = new ExternalSourcesBlockedFragment(); + fragment.setArguments(args); + return fragment; } @Override @@ -56,6 +84,8 @@ public class ExternalSourcesBlockedFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + setDialogData(requireArguments()); + Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData); mDialog = new AlertDialog.Builder(requireContext()) .setTitle(mDialogData.getAppLabel()) @@ -96,4 +126,14 @@ public class ExternalSourcesBlockedFragment extends DialogFragment { super.onResume(); mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); } + + private void setDialogData(Bundle args) { + int actionReason = args.getInt(ARGS_ACTION_REASON); + AppSnippet appSnippet = args.getParcelable(ARGS_APP_SNIPPET, AppSnippet.class); + boolean isUpdating = args.getBoolean(ARGS_IS_UPDATING); + String sourceApp = args.getString(ARGS_SOURCE_APP); + + mDialogData = new InstallUserActionRequired(actionReason, appSnippet, isUpdating, + sourceApp); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java index e186590fa5e2..03768fb56bb8 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java @@ -16,6 +16,11 @@ package com.android.packageinstaller.v2.ui.fragments; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_ACTION_REASON; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_APP_SNIPPET; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_IS_UPDATING; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_SOURCE_APP; + import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; @@ -26,11 +31,14 @@ import android.text.method.ScrollingMovementMethod; import android.util.Log; import android.view.View; import android.widget.TextView; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; + import com.android.packageinstaller.R; import com.android.packageinstaller.v2.model.InstallUserActionRequired; +import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet; import com.android.packageinstaller.v2.ui.InstallActionListener; /** @@ -39,15 +47,34 @@ import com.android.packageinstaller.v2.ui.InstallActionListener; public class InstallConfirmationFragment extends DialogFragment { public static final String LOG_TAG = InstallConfirmationFragment.class.getSimpleName(); - @NonNull - private final InstallUserActionRequired mDialogData; + private InstallUserActionRequired mDialogData; @NonNull private InstallActionListener mInstallActionListener; @NonNull private AlertDialog mDialog; - public InstallConfirmationFragment(@NonNull InstallUserActionRequired dialogData) { - mDialogData = dialogData; + public InstallConfirmationFragment() { + // Required for DialogFragment + } + + /** + * Creates a new instance of this fragment with necessary data set as fragment arguments + * + * @param dialogData {@link InstallUserActionRequired} object containing data to display + * in the dialog + * @return an instance of the fragment + */ + public static InstallConfirmationFragment newInstance( + @NonNull InstallUserActionRequired dialogData) { + Bundle args = new Bundle(); + args.putInt(ARGS_ACTION_REASON, dialogData.getActionReason()); + args.putParcelable(ARGS_APP_SNIPPET, dialogData.getAppSnippet()); + args.putBoolean(ARGS_IS_UPDATING, dialogData.isAppUpdating()); + args.putString(ARGS_SOURCE_APP, dialogData.getSourceApp()); + + InstallConfirmationFragment fragment = new InstallConfirmationFragment(); + fragment.setArguments(args); + return fragment; } @Override @@ -59,6 +86,8 @@ public class InstallConfirmationFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + setDialogData(requireArguments()); + Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData); View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null); @@ -127,4 +156,14 @@ public class InstallConfirmationFragment extends DialogFragment { super.onResume(); mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); } + + private void setDialogData(Bundle args) { + int actionReason = args.getInt(ARGS_ACTION_REASON); + AppSnippet appSnippet = args.getParcelable(ARGS_APP_SNIPPET, AppSnippet.class); + boolean isUpdating = args.getBoolean(ARGS_IS_UPDATING); + String sourceApp = args.getString(ARGS_SOURCE_APP); + + mDialogData = new InstallUserActionRequired(actionReason, appSnippet, isUpdating, + sourceApp); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java index 7c9d98dd4823..6f65441afd88 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java @@ -16,19 +16,30 @@ package com.android.packageinstaller.v2.ui.fragments; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_APP_SNIPPET; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_LEGACY_CODE; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_MESSAGE; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_RESULT_INTENT; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_SHOULD_RETURN_RESULT; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_STATUS_CODE; + import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.content.pm.PackageInstaller; import android.os.Bundle; import android.util.Log; import android.view.View; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; + import com.android.packageinstaller.R; import com.android.packageinstaller.v2.model.InstallFailed; +import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet; import com.android.packageinstaller.v2.ui.InstallActionListener; /** @@ -39,11 +50,32 @@ import com.android.packageinstaller.v2.ui.InstallActionListener; public class InstallFailedFragment extends DialogFragment { private static final String LOG_TAG = InstallFailedFragment.class.getSimpleName(); - private final InstallFailed mDialogData; + private InstallFailed mDialogData; private InstallActionListener mInstallActionListener; - public InstallFailedFragment(InstallFailed dialogData) { - mDialogData = dialogData; + public InstallFailedFragment() { + // Required for DialogFragment + } + + /** + * Creates a new instance of this fragment with necessary data set as fragment arguments + * + * @param dialogData {@link InstallFailed} object containing data to display in the + * dialog + * @return an instance of the fragment + */ + public static InstallFailedFragment newInstance(@NonNull InstallFailed dialogData) { + Bundle args = new Bundle(); + args.putParcelable(ARGS_APP_SNIPPET, dialogData.getAppSnippet()); + args.putInt(ARGS_LEGACY_CODE, dialogData.getLegacyCode()); + args.putInt(ARGS_STATUS_CODE, dialogData.getStatusCode()); + args.putString(ARGS_MESSAGE, dialogData.getMessage()); + args.putBoolean(ARGS_SHOULD_RETURN_RESULT, dialogData.getShouldReturnResult()); + args.putParcelable(ARGS_RESULT_INTENT, dialogData.getResultIntent()); + + InstallFailedFragment fragment = new InstallFailedFragment(); + fragment.setArguments(args); + return fragment; } @Override @@ -55,6 +87,8 @@ public class InstallFailedFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + setDialogData(requireArguments()); + Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData); View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null); AlertDialog dialog = new AlertDialog.Builder(requireContext()) @@ -105,4 +139,16 @@ public class InstallFailedFragment extends DialogFragment { super.onCancel(dialog); mInstallActionListener.onNegativeResponse(mDialogData.getStageCode()); } + + private void setDialogData(Bundle args) { + AppSnippet appSnippet = args.getParcelable(ARGS_APP_SNIPPET, AppSnippet.class); + int legacyCode = args.getInt(ARGS_LEGACY_CODE); + int statusCode = args.getInt(ARGS_STATUS_CODE); + String message = args.getString(ARGS_MESSAGE); + boolean shouldReturnResult = args.getBoolean(ARGS_SHOULD_RETURN_RESULT); + Intent resultIntent = args.getParcelable(ARGS_RESULT_INTENT, Intent.class); + + mDialogData = new InstallFailed(appSnippet, legacyCode, statusCode, message, + shouldReturnResult, resultIntent); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java index 27210b757181..17093cf16125 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java @@ -16,17 +16,22 @@ package com.android.packageinstaller.v2.ui.fragments; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_APP_SNIPPET; + import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; import android.view.View; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; + import com.android.packageinstaller.R; import com.android.packageinstaller.v2.model.InstallInstalling; +import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet; /** * Dialog to show when an install is in progress. @@ -34,16 +39,34 @@ import com.android.packageinstaller.v2.model.InstallInstalling; public class InstallInstallingFragment extends DialogFragment { private static final String LOG_TAG = InstallInstallingFragment.class.getSimpleName(); - private final InstallInstalling mDialogData; + private InstallInstalling mDialogData; private AlertDialog mDialog; - public InstallInstallingFragment(InstallInstalling dialogData) { - mDialogData = dialogData; + public InstallInstallingFragment() { + // Required for DialogFragment + } + + /** + * Creates a new instance of this fragment with necessary data set as fragment arguments + * + * @param dialogData {@link InstallInstalling} object containing data to display in the + * dialog + * @return an instance of the fragment + */ + public static InstallInstallingFragment newInstance(@NonNull InstallInstalling dialogData) { + Bundle args = new Bundle(); + args.putParcelable(ARGS_APP_SNIPPET, dialogData.getAppSnippet()); + + InstallInstallingFragment fragment = new InstallInstallingFragment(); + fragment.setArguments(args); + return fragment; } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + setDialogData(requireArguments()); + Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData); View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null); mDialog = new AlertDialog.Builder(requireContext()) @@ -64,4 +87,9 @@ public class InstallInstallingFragment extends DialogFragment { super.onStart(); mDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(false); } + + private void setDialogData(Bundle args) { + AppSnippet appSnippet = args.getParcelable(ARGS_APP_SNIPPET, AppSnippet.class); + mDialogData = new InstallInstalling(appSnippet); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java index 28b5423b2d83..5696afa52622 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java @@ -16,22 +16,31 @@ package com.android.packageinstaller.v2.ui.fragments; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_APP_SNIPPET; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_RESULT_INTENT; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_SHOULD_RETURN_RESULT; + import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; + import com.android.packageinstaller.R; import com.android.packageinstaller.v2.model.InstallSuccess; +import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet; import com.android.packageinstaller.v2.ui.InstallActionListener; + import java.util.List; /** @@ -41,13 +50,31 @@ import java.util.List; public class InstallSuccessFragment extends DialogFragment { private static final String LOG_TAG = InstallSuccessFragment.class.getSimpleName(); - private final InstallSuccess mDialogData; + private InstallSuccess mDialogData; private AlertDialog mDialog; private InstallActionListener mInstallActionListener; private PackageManager mPm; - public InstallSuccessFragment(InstallSuccess dialogData) { - mDialogData = dialogData; + public InstallSuccessFragment() { + // Required for DialogFragment + } + + /** + * Create a new instance of this fragment with necessary data set as fragment arguments + * + * @param dialogData {@link InstallSuccess} object containing data to display in the + * dialog + * @return an instance of the fragment + */ + public static InstallSuccessFragment newInstance(@NonNull InstallSuccess dialogData) { + Bundle args = new Bundle(); + args.putParcelable(ARGS_APP_SNIPPET, dialogData.getAppSnippet()); + args.putBoolean(ARGS_SHOULD_RETURN_RESULT, dialogData.getShouldReturnResult()); + args.putParcelable(ARGS_RESULT_INTENT, dialogData.getResultIntent()); + + InstallSuccessFragment fragment = new InstallSuccessFragment(); + fragment.setArguments(args); + return fragment; } @Override @@ -60,6 +87,8 @@ public class InstallSuccessFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + setDialogData(requireArguments()); + Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData); View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null); mDialog = new AlertDialog.Builder(requireContext()) @@ -105,4 +134,12 @@ public class InstallSuccessFragment extends DialogFragment { Log.i(LOG_TAG, "Finished installing " + mDialogData.getAppLabel()); mInstallActionListener.onNegativeResponse(mDialogData.getStageCode()); } + + private void setDialogData(Bundle args) { + AppSnippet appSnippet = args.getParcelable(ARGS_APP_SNIPPET, AppSnippet.class); + boolean shouldReturnResult = args.getBoolean(ARGS_SHOULD_RETURN_RESULT); + Intent resultIntent = args.getParcelable(ARGS_RESULT_INTENT, Intent.class); + + mDialogData = new InstallSuccess(appSnippet, shouldReturnResult, resultIntent); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ParseErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ParseErrorFragment.java index cde3d8d9dd2d..6834f44a37cf 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ParseErrorFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ParseErrorFragment.java @@ -16,14 +16,23 @@ package com.android.packageinstaller.v2.ui.fragments; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_ABORT_REASON; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_ACTIVITY_RESULT_CODE; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_ERROR_DIALOG_TYPE; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_MESSAGE; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_RESULT_INTENT; + import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.os.Bundle; import android.util.Log; + import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; + import com.android.packageinstaller.R; import com.android.packageinstaller.v2.model.InstallAborted; import com.android.packageinstaller.v2.ui.InstallActionListener; @@ -31,11 +40,31 @@ import com.android.packageinstaller.v2.ui.InstallActionListener; public class ParseErrorFragment extends DialogFragment { private static final String LOG_TAG = ParseErrorFragment.class.getSimpleName(); - private final InstallAborted mDialogData; + private InstallAborted mDialogData; private InstallActionListener mInstallActionListener; - public ParseErrorFragment(InstallAborted dialogData) { - mDialogData = dialogData; + public ParseErrorFragment() { + // Required for DialogFragment + } + + /** + * Create a new instance of this fragment with necessary data set as fragment arguments + * + * @param dialogData {@link InstallAborted} object containing data to display in the + * dialog + * @return an instance of the fragment + */ + public static ParseErrorFragment newInstance(@NonNull InstallAborted dialogData) { + Bundle args = new Bundle(); + args.putInt(ARGS_ABORT_REASON, dialogData.getAbortReason()); + args.putString(ARGS_MESSAGE, dialogData.getMessage()); + args.putParcelable(ARGS_RESULT_INTENT, dialogData.getResultIntent()); + args.putInt(ARGS_ACTIVITY_RESULT_CODE, dialogData.getActivityResultCode()); + args.putInt(ARGS_ERROR_DIALOG_TYPE, dialogData.getErrorDialogType()); + + ParseErrorFragment fragment = new ParseErrorFragment(); + fragment.setArguments(args); + return fragment; } @Override @@ -47,6 +76,8 @@ public class ParseErrorFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { + setDialogData(requireArguments()); + Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData); return new AlertDialog.Builder(requireContext()) .setMessage(R.string.Parse_error_dlg_text) @@ -63,4 +94,15 @@ public class ParseErrorFragment extends DialogFragment { mInstallActionListener.onNegativeResponse( mDialogData.getActivityResultCode(), mDialogData.getResultIntent()); } + + private void setDialogData(Bundle args) { + int abortReason = args.getInt(ARGS_ABORT_REASON); + String message = args.getString(ARGS_MESSAGE); + Intent resultIntent = args.getParcelable(ARGS_RESULT_INTENT, Intent.class); + int activityResultCode = args.getInt(ARGS_ACTIVITY_RESULT_CODE); + int errorDialogType = args.getInt(ARGS_ERROR_DIALOG_TYPE); + + mDialogData = new InstallAborted(abortReason, message, resultIntent, activityResultCode, + errorDialogType); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java index 66a353a3519e..8b1ccd8ab6e9 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java @@ -16,14 +16,18 @@ package com.android.packageinstaller.v2.ui.fragments; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_MESSAGE; + import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; + import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; + import com.android.packageinstaller.R; import com.android.packageinstaller.v2.model.InstallStage; import com.android.packageinstaller.v2.ui.InstallActionListener; @@ -31,11 +35,25 @@ import com.android.packageinstaller.v2.ui.InstallActionListener; public class SimpleErrorFragment extends DialogFragment { private static final String LOG_TAG = SimpleErrorFragment.class.getSimpleName(); - private final int mMessageResId; + private int mMessageResId; private InstallActionListener mInstallActionListener; - public SimpleErrorFragment(int messageResId) { - mMessageResId = messageResId; + public SimpleErrorFragment() { + // Required for DialogFragment + } + + /** + * Create a new instance of this fragment with necessary data set as fragment arguments + * + * @return an instance of the fragment + */ + public static SimpleErrorFragment newInstance(int messageResId) { + Bundle args = new Bundle(); + args.putInt(ARGS_MESSAGE, messageResId); + + SimpleErrorFragment fragment = new SimpleErrorFragment(); + fragment.setArguments(args); + return fragment; } @Override @@ -47,6 +65,8 @@ public class SimpleErrorFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { + mMessageResId = requireArguments().getInt(ARGS_MESSAGE); + Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + "Dialog message: " + requireContext().getString(mMessageResId)); return new AlertDialog.Builder(requireContext()) diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java index 524b4e6a0e63..860f6a085909 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java @@ -18,6 +18,11 @@ package com.android.packageinstaller.v2.ui.fragments; import static android.text.format.Formatter.formatFileSize; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_APP_DATA_SIZE; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_IS_ARCHIVE; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_MESSAGE; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_TITLE; + import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; @@ -27,9 +32,11 @@ import android.util.Log; import android.view.View; import android.widget.CheckBox; import android.widget.TextView; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; + import com.android.packageinstaller.R; import com.android.packageinstaller.v2.model.UninstallUserActionRequired; import com.android.packageinstaller.v2.ui.UninstallActionListener; @@ -38,14 +45,34 @@ import com.android.packageinstaller.v2.ui.UninstallActionListener; * Dialog to show while requesting user confirmation for uninstalling an app. */ public class UninstallConfirmationFragment extends DialogFragment { + private static final String LOG_TAG = UninstallConfirmationFragment.class.getSimpleName(); - private final UninstallUserActionRequired mDialogData; + private UninstallUserActionRequired mDialogData; private UninstallActionListener mUninstallActionListener; - private CheckBox mKeepData; - public UninstallConfirmationFragment(UninstallUserActionRequired dialogData) { - mDialogData = dialogData; + public UninstallConfirmationFragment() { + // Required for DialogFragment + } + + /** + * Create a new instance of this fragment with necessary data set as fragment arguments + * + * @param dialogData {@link UninstallUserActionRequired} object containing data to + * display in the dialog + * @return an instance of the fragment + */ + public static UninstallConfirmationFragment newInstance( + @NonNull UninstallUserActionRequired dialogData) { + Bundle args = new Bundle(); + args.putLong(ARGS_APP_DATA_SIZE, dialogData.getAppDataSize()); + args.putBoolean(ARGS_IS_ARCHIVE, dialogData.isArchive()); + args.putString(ARGS_TITLE, dialogData.getTitle()); + args.putString(ARGS_MESSAGE, dialogData.getMessage()); + + UninstallConfirmationFragment fragment = new UninstallConfirmationFragment(); + fragment.setArguments(args); + return fragment; } @Override @@ -57,6 +84,8 @@ public class UninstallConfirmationFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + setDialogData(requireArguments()); + Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData); AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()) .setTitle(mDialogData.getTitle()) @@ -88,4 +117,13 @@ public class UninstallConfirmationFragment extends DialogFragment { super.onCancel(dialog); mUninstallActionListener.onNegativeResponse(); } + + private void setDialogData(Bundle args) { + long appDataSize = args.getLong(ARGS_APP_DATA_SIZE); + boolean isArchive = args.getBoolean(ARGS_IS_ARCHIVE); + String title = args.getString(ARGS_TITLE); + String message = args.getString(ARGS_MESSAGE); + + mDialogData = new UninstallUserActionRequired(title, message, appDataSize, isArchive); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java index 51e16cbff55d..9ed64128088c 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java @@ -16,15 +16,19 @@ package com.android.packageinstaller.v2.ui.fragments; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_ABORT_REASON; + import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; + import com.android.packageinstaller.R; import com.android.packageinstaller.v2.model.UninstallAborted; import com.android.packageinstaller.v2.ui.UninstallActionListener; @@ -35,11 +39,27 @@ import com.android.packageinstaller.v2.ui.UninstallActionListener; public class UninstallErrorFragment extends DialogFragment { private static final String LOG_TAG = UninstallErrorFragment.class.getSimpleName(); - private final UninstallAborted mDialogData; + private UninstallAborted mDialogData; private UninstallActionListener mUninstallActionListener; - public UninstallErrorFragment(UninstallAborted dialogData) { - mDialogData = dialogData; + public UninstallErrorFragment() { + // Required for DialogFragment + } + + /** + * Create a new instance of this fragment with necessary data set as fragment arguments + * + * @param dialogData {@link UninstallAborted} object containing data to display in the + * dialog + * @return an instance of the fragment + */ + public static UninstallErrorFragment newInstance(UninstallAborted dialogData) { + Bundle args = new Bundle(); + args.putInt(ARGS_ABORT_REASON, dialogData.getAbortReason()); + + UninstallErrorFragment fragment = new UninstallErrorFragment(); + fragment.setArguments(args); + return fragment; } @Override @@ -51,6 +71,8 @@ public class UninstallErrorFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + setDialogData(requireArguments()); + Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData); AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()) .setMessage(mDialogData.getDialogTextResource()) @@ -68,4 +90,9 @@ public class UninstallErrorFragment extends DialogFragment { super.onCancel(dialog); mUninstallActionListener.onNegativeResponse(); } + + private void setDialogData(Bundle args) { + int abortReason = args.getInt(ARGS_ABORT_REASON); + mDialogData = new UninstallAborted(abortReason); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java index 626ff6b92f13..ae56c4d786b8 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java @@ -16,12 +16,17 @@ package com.android.packageinstaller.v2.ui.fragments; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_APP_LABEL; +import static com.android.packageinstaller.v2.model.PackageUtil.ARGS_IS_CLONE_USER; + import android.app.AlertDialog; import android.app.Dialog; import android.os.Bundle; import android.util.Log; + import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; + import com.android.packageinstaller.R; import com.android.packageinstaller.v2.model.UninstallUninstalling; @@ -33,13 +38,32 @@ public class UninstallUninstallingFragment extends DialogFragment { private static final String LOG_TAG = UninstallUninstallingFragment.class.getSimpleName(); UninstallUninstalling mDialogData; - public UninstallUninstallingFragment(UninstallUninstalling dialogData) { - mDialogData = dialogData; + UninstallUninstallingFragment() { + // Required for DialogFragment + } + + /** + * Create a new instance of this fragment with necessary data set as fragment arguments + * + * @param dialogData {@link UninstallUninstalling} object containing data to display in + * the dialog + * @return an instance of the fragment + */ + public static UninstallUninstallingFragment newInstance(UninstallUninstalling dialogData) { + Bundle args = new Bundle(); + args.putCharSequence(ARGS_APP_LABEL, dialogData.getAppLabel()); + args.putBoolean(ARGS_IS_CLONE_USER, dialogData.isCloneUser()); + + UninstallUninstallingFragment fragment = new UninstallUninstallingFragment(); + fragment.setArguments(args); + return fragment; } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { + setDialogData(requireArguments()); + Log.i(LOG_TAG, "Creating " + LOG_TAG + "\n" + mDialogData); AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()) .setCancelable(false); @@ -55,4 +79,11 @@ public class UninstallUninstallingFragment extends DialogFragment { return dialog; } + + private void setDialogData(Bundle args) { + CharSequence label = args.getCharSequence(ARGS_APP_LABEL); + boolean isCloneUser = args.getBoolean(ARGS_IS_CLONE_USER); + + mDialogData = new UninstallUninstalling(label, isCloneUser); + } } diff --git a/packages/SettingsLib/AndroidManifest.xml b/packages/SettingsLib/AndroidManifest.xml index 412ff29aec32..473b7eb07666 100644 --- a/packages/SettingsLib/AndroidManifest.xml +++ b/packages/SettingsLib/AndroidManifest.xml @@ -21,6 +21,13 @@ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> <application> + <activity + android:name=".users.CreateUserActivity" + android:excludeFromRecents="true" + android:exported="false" + android:finishOnCloseSystemDialogs="true" + android:launchMode="singleInstance" + android:theme="@style/Theme.Transparent"/> </application> </manifest> diff --git a/packages/SettingsLib/BannerMessagePreference/Android.bp b/packages/SettingsLib/BannerMessagePreference/Android.bp index 182daeb45d7c..203a3bf64910 100644 --- a/packages/SettingsLib/BannerMessagePreference/Android.bp +++ b/packages/SettingsLib/BannerMessagePreference/Android.bp @@ -31,5 +31,6 @@ android_library { apex_available: [ "//apex_available:platform", "com.android.healthfitness", + "com.android.permission", ], } diff --git a/packages/SettingsLib/BannerMessagePreference/res/drawable-v35/settingslib_resolved_banner_avd.xml b/packages/SettingsLib/BannerMessagePreference/res/drawable-v35/settingslib_resolved_banner_avd.xml new file mode 100644 index 000000000000..c999de7d99ea --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/drawable-v35/settingslib_resolved_banner_avd.xml @@ -0,0 +1,157 @@ +<!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:width="112dp" + android:height="112dp" + android:viewportHeight="112" + android:viewportWidth="112"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:translateX="56.5" + android:translateY="56.625"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="0" + android:fillColor="?android:attr/textColorPrimary" + android:fillType="nonZero" + android:pathData=" M-14.75 -0.59 C-14.75,-0.59 -15.32,-1.16 -15.32,-1.16 C-15.32,-1.16 -14.67,-0.73 -14.67,-0.73 C-14.67,-0.73 -14.39,-0.44 -14.39,-0.44 C-14.39,-0.44 -17.32,2.5 -17.32,2.5 C-17.32,2.5 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:trimPathEnd="1" + android:trimPathOffset="0" + android:trimPathStart="0" /> + </group> + <group + android:name="_R_G_L_0_G" + android:rotation="-90" + android:translateX="56" + android:translateY="56"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:pathData=" M53.5 0 C53.5,-29.53 29.53,-53.5 0,-53.5 C0,-53.5 0,-53.5 0,-53.5 C-29.53,-53.5 -53.5,-29.53 -53.5,0 C-53.5,0 -53.5,0 -53.5,0 C-53.5,29.53 -29.53,53.5 0,53.5 C0,53.5 0,53.5 0,53.5 C29.53,53.5 53.5,29.53 53.5,0 C53.5,0 53.5,0 53.5,0c " + android:strokeAlpha="1" + android:strokeColor="?android:attr/textColorTertiary" + android:strokeLineCap="round" + android:strokeLineJoin="round" + android:strokeWidth="5" + android:trimPathEnd="0" + android:trimPathOffset="0" + android:trimPathStart="0" /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="400" + android:propertyName="fillAlpha" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="0" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="fillAlpha" + android:startOffset="400" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="400" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-14.75 -0.59 C-14.75,-0.59 -15.32,-1.16 -15.32,-1.16 C-15.32,-1.16 -14.67,-0.73 -14.67,-0.73 C-14.67,-0.73 -14.39,-0.44 -14.39,-0.44 C-14.39,-0.44 -17.32,2.5 -17.32,2.5 C-17.32,2.5 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueTo="M-14.75 -0.59 C-14.75,-0.59 -15.32,-1.16 -15.32,-1.16 C-15.32,-1.16 -14.67,-0.73 -14.67,-0.73 C-14.67,-0.73 -14.39,-0.44 -14.39,-0.44 C-14.39,-0.44 -17.32,2.5 -17.32,2.5 C-17.32,2.5 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="83" + android:propertyName="pathData" + android:startOffset="400" + android:valueFrom="M-14.75 -0.59 C-14.75,-0.59 -15.32,-1.16 -15.32,-1.16 C-15.32,-1.16 -14.67,-0.73 -14.67,-0.73 C-14.67,-0.73 -14.39,-0.44 -14.39,-0.44 C-14.39,-0.44 -17.32,2.5 -17.32,2.5 C-17.32,2.5 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueTo="M-14.75 -0.59 C-14.75,-0.59 -6.41,7.75 -6.41,7.75 C-6.41,7.75 -6.3,7.65 -6.3,7.65 C-6.3,7.65 -3.48,10.47 -3.48,10.47 C-3.48,10.47 -6.41,13.41 -6.41,13.41 C-6.41,13.41 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="167" + android:propertyName="pathData" + android:startOffset="483" + android:valueFrom="M-14.75 -0.59 C-14.75,-0.59 -6.41,7.75 -6.41,7.75 C-6.41,7.75 -6.3,7.65 -6.3,7.65 C-6.3,7.65 -3.48,10.47 -3.48,10.47 C-3.48,10.47 -6.41,13.41 -6.41,13.41 C-6.41,13.41 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueTo="M-14.75 -0.59 C-14.75,-0.59 -6.41,7.75 -6.41,7.75 C-6.41,7.75 14.77,-13.41 14.77,-13.41 C14.77,-13.41 17.59,-10.59 17.59,-10.59 C17.59,-10.59 -6.41,13.41 -6.41,13.41 C-6.41,13.41 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="500" + android:propertyName="trimPathEnd" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="667" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/layout-v35/settingslib_expressive_banner_message.xml b/packages/SettingsLib/BannerMessagePreference/res/layout-v35/settingslib_expressive_banner_message.xml index b10ef6e77ec7..c448a2d434f8 100644 --- a/packages/SettingsLib/BannerMessagePreference/res/layout-v35/settingslib_expressive_banner_message.xml +++ b/packages/SettingsLib/BannerMessagePreference/res/layout-v35/settingslib_expressive_banner_message.xml @@ -28,81 +28,104 @@ android:orientation="vertical" style="@style/Banner.Preference.SettingsLib.Expressive"> - <RelativeLayout - android:id="@+id/top_row" + <FrameLayout android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal"> + android:layout_height="wrap_content"> <LinearLayout + android:id="@+id/banner_content" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentStart="true" - android:layout_marginEnd="@dimen/settingslib_expressive_space_medium4" android:orientation="vertical"> - <TextView - android:id="@+id/banner_header" + + <RelativeLayout + android:id="@+id/top_row" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/Banner.Header.SettingsLib.Expressive"/> + android:orientation="horizontal"> - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content"> - - <ImageView - android:id="@+id/banner_icon" - android:layout_width="@dimen/settingslib_expressive_space_small3" - android:layout_height="@dimen/settingslib_expressive_space_small3" - android:layout_gravity="center_vertical" - android:importantForAccessibility="no" - android:scaleType="fitCenter" /> - - <TextView - android:id="@+id/banner_title" + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/Banner.Title.SettingsLib.Expressive" /> - </LinearLayout> + android:layout_alignParentStart="true" + android:layout_marginEnd="@dimen/settingslib_expressive_space_medium4" + android:orientation="vertical"> + <TextView + android:id="@+id/banner_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/Banner.Header.SettingsLib.Expressive"/> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/banner_icon" + android:layout_width="@dimen/settingslib_expressive_space_small3" + android:layout_height="@dimen/settingslib_expressive_space_small3" + android:layout_gravity="center_vertical" + android:layout_marginEnd="@dimen/settingslib_expressive_space_extrasmall4" + android:importantForAccessibility="no" + android:scaleType="fitCenter" /> + + <TextView + android:id="@+id/banner_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/Banner.Title.SettingsLib.Expressive" /> + </LinearLayout> + + <TextView + android:id="@+id/banner_subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/Banner.Subtitle.SettingsLib.Expressive"/> + </LinearLayout> + + <ImageButton + android:id="@+id/banner_dismiss_btn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/accessibility_banner_message_dismiss" + style="@style/Banner.Dismiss.SettingsLib.Expressive" /> + </RelativeLayout> <TextView - android:id="@+id/banner_subtitle" + android:id="@+id/banner_summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/Banner.Summary.SettingsLib.Expressive"/> + + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/Banner.Subtitle.SettingsLib.Expressive"/> + android:id="@+id/banner_buttons_frame" + android:paddingTop="@dimen/settingslib_expressive_space_extrasmall6" + android:orientation="horizontal"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/banner_negative_btn" + android:layout_weight="1" + style="@style/Banner.NegativeButton.SettingsLib.Expressive"/> + <Space + android:id="@+id/banner_button_space" + android:layout_width="@dimen/settingslib_expressive_space_extrasmall4" + android:layout_height="@dimen/settingslib_expressive_space_small1"/> + <com.google.android.material.button.MaterialButton + android:id="@+id/banner_positive_btn" + android:layout_weight="1" + style="@style/Banner.PositiveButton.SettingsLib.Expressive"/> + </LinearLayout> </LinearLayout> - <ImageButton - android:id="@+id/banner_dismiss_btn" + <TextView + android:id="@+id/resolved_banner_text" android:layout_width="wrap_content" android:layout_height="wrap_content" - style="@style/Banner.Dismiss.SettingsLib.Expressive" /> - </RelativeLayout> - - <TextView - android:id="@+id/banner_summary" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@style/Banner.Summary.SettingsLib.Expressive"/> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/banner_buttons_frame" - android:paddingTop="@dimen/settingslib_expressive_space_extrasmall6" - android:orientation="horizontal"> - - <com.google.android.material.button.MaterialButton - android:id="@+id/banner_negative_btn" - android:layout_weight="1" - style="@style/Banner.NegativeButton.SettingsLib.Expressive"/> - <Space - android:layout_width="@dimen/settingslib_expressive_space_extrasmall4" - android:layout_height="@dimen/settingslib_expressive_space_small1"/> - <com.google.android.material.button.MaterialButton - android:id="@+id/banner_positive_btn" - android:layout_weight="1" - style="@style/Banner.PositiveButton.SettingsLib.Expressive"/> - </LinearLayout> + android:drawableTop="@drawable/settingslib_resolved_banner_avd" + android:visibility="gone" + style="@style/Banner.ResolvedText.SettingsLib.Expressive"/> + </FrameLayout> </com.android.settingslib.widget.BannerMessageView> </LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/values-v35/styles_expressive.xml b/packages/SettingsLib/BannerMessagePreference/res/values-v35/styles_expressive.xml index b864311bf9b7..09e07ccef683 100644 --- a/packages/SettingsLib/BannerMessagePreference/res/values-v35/styles_expressive.xml +++ b/packages/SettingsLib/BannerMessagePreference/res/values-v35/styles_expressive.xml @@ -33,7 +33,6 @@ <style name="Banner.Title.SettingsLib.Expressive" parent=""> <item name="android:layout_gravity">start</item> - <item name="android:layout_marginLeft">@dimen/settingslib_expressive_space_extrasmall4</item> <item name="android:textAlignment">viewStart</item> <item name="android:textAppearance">@style/TextAppearance.SettingsLib.TitleLarge.Emphasized</item> <item name="android:textColor">?android:attr/textColorPrimary</item> @@ -73,4 +72,11 @@ parent="@style/SettingsLibButtonStyle.Expressive.Outline.Extra"> <item name="materialSizeOverlay">@style/SizeOverlay.Material3Expressive.Button.Small</item> </style> + + <style name="Banner.ResolvedText.SettingsLib.Expressive" parent=""> + <item name="android:layout_gravity">center</item> + <item name="android:drawablePadding">@dimen/settingslib_expressive_space_small1</item> + <item name="android:textAppearance">@style/TextAppearance.SettingsLib.BodyMedium</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + </style> </resources> diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java index c82829d6ccea..c90a76a39510 100644 --- a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java +++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java @@ -35,6 +35,8 @@ import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.preference.Preference; @@ -43,6 +45,9 @@ import androidx.preference.PreferenceViewHolder; import com.android.settingslib.widget.preference.banner.R; import com.google.android.material.button.MaterialButton; + +import java.util.Objects; + /** * Banner message is a banner displaying important information (permission request, page error etc), * and provide actions for user to address. It requires a user action to be dismissed. @@ -67,13 +72,15 @@ public class BannerMessagePreference extends Preference implements GroupSectionD R.color.banner_accent_attention_normal, R.color.settingslib_banner_button_background_normal); - // Corresponds to the enum valye of R.attr.attentionLevel + // Corresponds to the enum value of R.attr.attentionLevel private final int mAttrValue; @ColorRes private final int mBackgroundColorResId; @ColorRes private final int mAccentColorResId; @ColorRes private final int mButtonBackgroundColorResId; - AttentionLevel(int attrValue, @ColorRes int backgroundColorResId, + AttentionLevel( + int attrValue, + @ColorRes int backgroundColorResId, @ColorRes int accentColorResId, @ColorRes int buttonBackgroundColorResId) { mAttrValue = attrValue; @@ -115,33 +122,38 @@ public class BannerMessagePreference extends Preference implements GroupSectionD new BannerMessagePreference.DismissButtonInfo(); // Default attention level is High. - private AttentionLevel mAttentionLevel = AttentionLevel.HIGH; - private CharSequence mSubtitle; - private CharSequence mHeader; + @NonNull private AttentionLevel mAttentionLevel = AttentionLevel.HIGH; + @Nullable private CharSequence mSubtitle; + @Nullable private CharSequence mHeader; private int mButtonOrientation; + @Nullable private ResolutionAnimator.Data mResolutionData; - public BannerMessagePreference(Context context) { + public BannerMessagePreference(@NonNull Context context) { super(context); init(context, null /* attrs */); } - public BannerMessagePreference(Context context, AttributeSet attrs) { + public BannerMessagePreference(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context, attrs); } - public BannerMessagePreference(Context context, AttributeSet attrs, int defStyleAttr) { + public BannerMessagePreference( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } - public BannerMessagePreference(Context context, AttributeSet attrs, int defStyleAttr, + public BannerMessagePreference( + @NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs); } - private void init(Context context, AttributeSet attrs) { + private void init(@NonNull Context context, @Nullable AttributeSet attrs) { setSelectable(false); int resId = SettingsThemeHelper.isExpressiveTheme(context) ? R.layout.settingslib_expressive_banner_message @@ -166,7 +178,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } @Override - public void onBindViewHolder(PreferenceViewHolder holder) { + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { super.onBindViewHolder(holder); final Context context = getContext(); @@ -193,14 +205,20 @@ public class BannerMessagePreference extends Preference implements GroupSectionD final ImageView iconView = (ImageView) holder.findViewById(R.id.banner_icon); if (iconView != null) { Drawable icon = getIcon(); - iconView.setImageDrawable( - icon == null - ? getContext().getDrawable(R.drawable.ic_warning) - : icon); - if (mAttentionLevel != AttentionLevel.NORMAL - && !SettingsThemeHelper.isExpressiveTheme(context)) { - iconView.setColorFilter( - new PorterDuffColorFilter(accentColor, PorterDuff.Mode.SRC_IN)); + + if (icon == null && SettingsThemeHelper.isExpressiveTheme(context)) { + iconView.setVisibility(View.GONE); + } else { + iconView.setVisibility(View.VISIBLE); + iconView.setImageDrawable( + icon == null + ? getContext().getDrawable(R.drawable.ic_warning) + : icon); + if (mAttentionLevel != AttentionLevel.NORMAL + && !SettingsThemeHelper.isExpressiveTheme(context)) { + iconView.setColorFilter( + new PorterDuffColorFilter(accentColor, PorterDuff.Mode.SRC_IN)); + } } } @@ -233,8 +251,10 @@ public class BannerMessagePreference extends Preference implements GroupSectionD mDismissButtonInfo.setUpButton(); final TextView subtitleView = (TextView) holder.findViewById(R.id.banner_subtitle); - subtitleView.setText(mSubtitle); - subtitleView.setVisibility(mSubtitle == null ? View.GONE : View.VISIBLE); + if (subtitleView != null) { + subtitleView.setText(mSubtitle); + subtitleView.setVisibility(mSubtitle == null ? View.GONE : View.VISIBLE); + } TextView headerView = (TextView) holder.findViewById(R.id.banner_header); if (headerView != null) { @@ -268,11 +288,25 @@ public class BannerMessagePreference extends Preference implements GroupSectionD linearLayout.setOrientation(mButtonOrientation); } } + + View buttonSpace = holder.findViewById(R.id.banner_button_space); + if (buttonSpace != null) { + if (mPositiveButtonInfo.shouldBeVisible() && mNegativeButtonInfo.shouldBeVisible()) { + buttonSpace.setVisibility(View.VISIBLE); + } else { + buttonSpace.setVisibility(View.GONE); + } + } + + if (mResolutionData != null) { + new ResolutionAnimator(mResolutionData, holder).startResolutionAnimation(); + } } /** - * Set the visibility state of positive button. + * Sets the visibility state of the positive button. */ + @NonNull public BannerMessagePreference setPositiveButtonVisible(boolean isVisible) { if (isVisible != mPositiveButtonInfo.mIsVisible) { mPositiveButtonInfo.mIsVisible = isVisible; @@ -282,8 +316,9 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Set the visibility state of negative button. + * Sets the visibility state of the negative button. */ + @NonNull public BannerMessagePreference setNegativeButtonVisible(boolean isVisible) { if (isVisible != mNegativeButtonInfo.mIsVisible) { mNegativeButtonInfo.mIsVisible = isVisible; @@ -293,9 +328,10 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Set the visibility state of dismiss button. + * Sets the visibility state of the dismiss button. */ @RequiresApi(Build.VERSION_CODES.S) + @NonNull public BannerMessagePreference setDismissButtonVisible(boolean isVisible) { if (isVisible != mDismissButtonInfo.mIsVisible) { mDismissButtonInfo.mIsVisible = isVisible; @@ -305,10 +341,35 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Register a callback to be invoked when positive button is clicked. + * Sets the enabled state of the positive button. + */ + @NonNull + public BannerMessagePreference setPositiveButtonEnabled(boolean isEnabled) { + if (isEnabled != mPositiveButtonInfo.mIsEnabled) { + mPositiveButtonInfo.mIsEnabled = isEnabled; + notifyChanged(); + } + return this; + } + + /** + * Sets the enabled state of the negative button. + */ + @NonNull + public BannerMessagePreference setNegativeButtonEnabled(boolean isEnabled) { + if (isEnabled != mNegativeButtonInfo.mIsEnabled) { + mNegativeButtonInfo.mIsEnabled = isEnabled; + notifyChanged(); + } + return this; + } + + /** + * Registers a callback to be invoked when positive button is clicked. */ + @NonNull public BannerMessagePreference setPositiveButtonOnClickListener( - View.OnClickListener listener) { + @Nullable View.OnClickListener listener) { if (listener != mPositiveButtonInfo.mListener) { mPositiveButtonInfo.mListener = listener; notifyChanged(); @@ -317,10 +378,11 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Register a callback to be invoked when negative button is clicked. + * Registers a callback to be invoked when negative button is clicked. */ + @NonNull public BannerMessagePreference setNegativeButtonOnClickListener( - View.OnClickListener listener) { + @Nullable View.OnClickListener listener) { if (listener != mNegativeButtonInfo.mListener) { mNegativeButtonInfo.mListener = listener; notifyChanged(); @@ -329,11 +391,12 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Register a callback to be invoked when the dismiss button is clicked. + * Registers a callback to be invoked when the dismiss button is clicked. */ @RequiresApi(Build.VERSION_CODES.S) + @NonNull public BannerMessagePreference setDismissButtonOnClickListener( - View.OnClickListener listener) { + @Nullable View.OnClickListener listener) { if (listener != mDismissButtonInfo.mListener) { mDismissButtonInfo.mListener = listener; notifyChanged(); @@ -344,6 +407,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the text to be displayed in positive button. */ + @NonNull public BannerMessagePreference setPositiveButtonText(@StringRes int textResId) { return setPositiveButtonText(getContext().getString(textResId)); } @@ -351,7 +415,9 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the text to be displayed in positive button. */ - public BannerMessagePreference setPositiveButtonText(CharSequence positiveButtonText) { + @NonNull + public BannerMessagePreference setPositiveButtonText( + @Nullable CharSequence positiveButtonText) { if (!TextUtils.equals(positiveButtonText, mPositiveButtonInfo.mText)) { mPositiveButtonInfo.mText = positiveButtonText; notifyChanged(); @@ -362,6 +428,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the text to be displayed in negative button. */ + @NonNull public BannerMessagePreference setNegativeButtonText(@StringRes int textResId) { return setNegativeButtonText(getContext().getString(textResId)); } @@ -369,7 +436,9 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the text to be displayed in negative button. */ - public BannerMessagePreference setNegativeButtonText(CharSequence negativeButtonText) { + @NonNull + public BannerMessagePreference setNegativeButtonText( + @Nullable CharSequence negativeButtonText) { if (!TextUtils.equals(negativeButtonText, mNegativeButtonInfo.mText)) { mNegativeButtonInfo.mText = negativeButtonText; notifyChanged(); @@ -380,8 +449,12 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets button orientation. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + @NonNull public BannerMessagePreference setButtonOrientation(int orientation) { + if (!SettingsThemeHelper.isExpressiveTheme(getContext())) { + return this; + } + if (mButtonOrientation != orientation) { mButtonOrientation = orientation; notifyChanged(); @@ -393,6 +466,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD * Sets the subtitle. */ @RequiresApi(Build.VERSION_CODES.S) + @NonNull public BannerMessagePreference setSubtitle(@StringRes int textResId) { return setSubtitle(getContext().getString(textResId)); } @@ -401,7 +475,8 @@ public class BannerMessagePreference extends Preference implements GroupSectionD * Sets the subtitle. */ @RequiresApi(Build.VERSION_CODES.S) - public BannerMessagePreference setSubtitle(CharSequence subtitle) { + @NonNull + public BannerMessagePreference setSubtitle(@Nullable CharSequence subtitle) { if (!TextUtils.equals(subtitle, mSubtitle)) { mSubtitle = subtitle; notifyChanged(); @@ -412,7 +487,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the header. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + @NonNull public BannerMessagePreference setHeader(@StringRes int textResId) { return setHeader(getContext().getString(textResId)); } @@ -420,8 +495,12 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the header. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) - public BannerMessagePreference setHeader(CharSequence header) { + @NonNull + public BannerMessagePreference setHeader(@Nullable CharSequence header) { + if (!SettingsThemeHelper.isExpressiveTheme(getContext())) { + return this; + } + if (!TextUtils.equals(header, mHeader)) { mHeader = header; notifyChanged(); @@ -430,32 +509,75 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Sets the attention level. This will update the color theme of the preference. + * Plays a resolution animation, showing the given message. */ - public BannerMessagePreference setAttentionLevel(AttentionLevel attentionLevel) { - if (attentionLevel == mAttentionLevel) { + @NonNull + public BannerMessagePreference showResolutionAnimation( + @NonNull CharSequence resolutionMessage, + @NonNull ResolutionCompletedCallback onCompletionCallback) { + if (!SettingsThemeHelper.isExpressiveTheme(getContext())) { return this; } + ResolutionAnimator.Data resolutionData = + new ResolutionAnimator.Data(resolutionMessage, onCompletionCallback); + if (!Objects.equals(mResolutionData, resolutionData)) { + mResolutionData = resolutionData; + notifyChanged(); + } + return this; + } - if (attentionLevel != null) { - mAttentionLevel = attentionLevel; + /** + * Removes the resolution animation from this preference. + * + * <p>Should be called after the resolution animation completes if this preference will be + * reused. Otherwise the resolution animation will be played everytime this preference is + * displayed. + */ + @NonNull + public BannerMessagePreference clearResolutionAnimation() { + if (!SettingsThemeHelper.isExpressiveTheme(getContext())) { + return this; + } + if (mResolutionData != null) { + mResolutionData = null; notifyChanged(); } return this; } + /** + * Sets the attention level. This will update the color theme of the preference. + */ + @NonNull + public BannerMessagePreference setAttentionLevel(@NonNull AttentionLevel attentionLevel) { + if (attentionLevel == mAttentionLevel) { + return this; + } + + mAttentionLevel = attentionLevel; + notifyChanged(); + return this; + } + static class ButtonInfo { - private Button mButton; - private CharSequence mText; - private View.OnClickListener mListener; + @Nullable private Button mButton; + @Nullable private CharSequence mText; + @Nullable private View.OnClickListener mListener; private boolean mIsVisible = true; + private boolean mIsEnabled = true; @ColorInt private int mColor; @ColorInt private int mBackgroundColor; - private ColorStateList mStrokeColor; + @Nullable private ColorStateList mStrokeColor; void setUpButton() { + if (mButton == null) { + return; + } + mButton.setText(mText); mButton.setOnClickListener(mListener); + mButton.setEnabled(mIsEnabled); MaterialButton btn = null; if (mButton instanceof MaterialButton) { @@ -492,11 +614,15 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } static class DismissButtonInfo { - private ImageButton mButton; - private View.OnClickListener mListener; + @Nullable private ImageButton mButton; + @Nullable private View.OnClickListener mListener; private boolean mIsVisible = true; void setUpButton() { + if (mButton == null) { + return; + } + mButton.setOnClickListener(mListener); if (shouldBeVisible()) { mButton.setVisibility(View.VISIBLE); diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessageView.java b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessageView.java index ff4e79ddaaa1..eabb6341c3dd 100644 --- a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessageView.java +++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessageView.java @@ -23,6 +23,7 @@ import android.view.TouchDelegate; import android.view.View; import android.widget.LinearLayout; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.settingslib.widget.preference.banner.R; @@ -34,22 +35,25 @@ import com.android.settingslib.widget.preference.banner.R; * {@link BannerMessagePreference} to a {@code PreferenceScreen}. */ public class BannerMessageView extends LinearLayout { - private Rect mTouchTargetForDismissButton; + @Nullable private Rect mTouchTargetForDismissButton; - public BannerMessageView(Context context) { + public BannerMessageView(@NonNull Context context) { super(context); } - public BannerMessageView(Context context, - @Nullable AttributeSet attrs) { + public BannerMessageView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } - public BannerMessageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + public BannerMessageView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } - public BannerMessageView(Context context, AttributeSet attrs, int defStyleAttr, + public BannerMessageView( + @NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/ResolutionAnimator.kt b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/ResolutionAnimator.kt new file mode 100644 index 000000000000..fbf910a42423 --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/ResolutionAnimator.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget + +import android.graphics.drawable.Animatable2 +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import android.provider.DeviceConfig +import android.transition.Fade +import android.transition.Transition +import android.transition.TransitionListenerAdapter +import android.transition.TransitionManager +import android.transition.TransitionSet +import android.view.View +import android.view.ViewGroup +import android.view.animation.LinearInterpolator +import android.widget.TextView +import androidx.preference.PreferenceViewHolder +import com.android.settingslib.widget.preference.banner.R +import java.time.Duration + +/** Callback to communicate when a banner message resolution animation is completed. */ +fun interface ResolutionCompletedCallback { + fun onCompleted() +} + +internal class ResolutionAnimator( + private val data: Data, + private val preferenceViewHolder: PreferenceViewHolder, +) { + + data class Data( + val resolutionMessage: CharSequence, + val resolutionCompletedCallback: ResolutionCompletedCallback, + ) + + private val defaultBannerContent: View? + get() = preferenceViewHolder.findView(R.id.banner_content) + private val resolvedTextView: TextView? + get() = preferenceViewHolder.findView(R.id.resolved_banner_text) + + fun startResolutionAnimation() { + resolvedTextView?.text = data.resolutionMessage + resolvedTextView?.resolutionDrawable?.reset() + + val transitionSet = + TransitionSet() + .setOrdering(TransitionSet.ORDERING_SEQUENTIAL) + .setInterpolator(linearInterpolator) + .addTransition(hideIssueContentTransition) + .addTransition( + showResolvedContentTransition + .clone() + .addListener( + object : TransitionListenerAdapter() { + override fun onTransitionEnd(transition: Transition) { + super.onTransitionEnd(transition) + startIssueResolvedAnimation() + } + } + ) + ) + + preferenceViewHolder.itemView.post { + TransitionManager.beginDelayedTransition( + preferenceViewHolder.itemView as ViewGroup, + transitionSet, + ) + + defaultBannerContent?.visibility = View.INVISIBLE + resolvedTextView?.visibility = View.VISIBLE + } + + preferenceViewHolder.itemView.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) {} + + override fun onViewDetachedFromWindow(v: View) { + v.removeOnAttachStateChangeListener(this) + cancelAnimationsAndFinish() + } + } + ) + } + + private fun startIssueResolvedAnimation() { + val animatedDrawable = resolvedTextView?.resolutionDrawable + + if (animatedDrawable == null) { + hideResolvedUiAndFinish() + return + } + + animatedDrawable.apply { + clearAnimationCallbacks() + registerAnimationCallback( + object : Animatable2.AnimationCallback() { + override fun onAnimationEnd(drawable: Drawable) { + super.onAnimationEnd(drawable) + hideResolvedUiAndFinish() + } + } + ) + start() + } + } + + private fun hideResolvedUiAndFinish() { + val hideTransition = + hideResolvedContentTransition + .clone() + .setInterpolator(linearInterpolator) + .addListener( + object : TransitionListenerAdapter() { + override fun onTransitionEnd(transition: Transition) { + super.onTransitionEnd(transition) + data.resolutionCompletedCallback.onCompleted() + } + } + ) + TransitionManager.beginDelayedTransition( + preferenceViewHolder.itemView as ViewGroup, + hideTransition, + ) + resolvedTextView?.visibility = View.GONE + } + + private fun cancelAnimationsAndFinish() { + TransitionManager.endTransitions(preferenceViewHolder.itemView as ViewGroup) + + resolvedTextView?.visibility = View.GONE + + val animatedDrawable = resolvedTextView?.resolutionDrawable + animatedDrawable?.clearAnimationCallbacks() + animatedDrawable?.stop() + + data.resolutionCompletedCallback.onCompleted() + } + + private companion object { + private val linearInterpolator = LinearInterpolator() + + private val HIDE_ISSUE_CONTENT_TRANSITION_DURATION = Duration.ofMillis(333) + private val hideIssueContentTransition = + Fade(Fade.OUT).setDuration(HIDE_ISSUE_CONTENT_TRANSITION_DURATION.toMillis()) + + private val SHOW_RESOLVED_CONTENT_TRANSITION_DELAY = Duration.ofMillis(133) + private val SHOW_RESOLVED_CONTENT_TRANSITION_DURATION = Duration.ofMillis(250) + private val showResolvedContentTransition = + Fade(Fade.IN) + .setStartDelay(SHOW_RESOLVED_CONTENT_TRANSITION_DELAY.toMillis()) + .setDuration(SHOW_RESOLVED_CONTENT_TRANSITION_DURATION.toMillis()) + + private val hideResolvedContentTransitionDelay + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Duration.ofMillis( + DeviceConfig.getLong( + "settings_ui", + "banner_message_pref_hide_resolved_content_delay_millis", + 400, + ) + ) + } else { + Duration.ofMillis(400) + } + + private val HIDE_RESOLVED_UI_TRANSITION_DURATION = Duration.ofMillis(167) + private val hideResolvedContentTransition + get() = + Fade(Fade.OUT) + .setStartDelay(hideResolvedContentTransitionDelay.toMillis()) + .setDuration(HIDE_RESOLVED_UI_TRANSITION_DURATION.toMillis()) + + inline fun <reified T : View> PreferenceViewHolder.findView(id: Int): T? = + findViewById(id) as? T + + val TextView.resolutionDrawable: AnimatedVectorDrawable? + get() = compoundDrawables.find { it != null } as? AnimatedVectorDrawable + } +} diff --git a/packages/SettingsLib/ButtonPreference/Android.bp b/packages/SettingsLib/ButtonPreference/Android.bp index a377f312ffbf..c8375a992d30 100644 --- a/packages/SettingsLib/ButtonPreference/Android.bp +++ b/packages/SettingsLib/ButtonPreference/Android.bp @@ -30,5 +30,6 @@ android_library { apex_available: [ "//apex_available:platform", "com.android.healthfitness", + "com.android.permission", ], } diff --git a/packages/SettingsLib/IllustrationPreference/AndroidManifest.xml b/packages/SettingsLib/IllustrationPreference/AndroidManifest.xml index a0d10c383445..56b05159a30f 100644 --- a/packages/SettingsLib/IllustrationPreference/AndroidManifest.xml +++ b/packages/SettingsLib/IllustrationPreference/AndroidManifest.xml @@ -18,6 +18,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.widget.preference.illustration"> - <uses-sdk android:minSdkVersion="28" /> + <uses-sdk android:minSdkVersion="30" /> </manifest> diff --git a/packages/SettingsLib/IllustrationPreference/res/values/strings.xml b/packages/SettingsLib/IllustrationPreference/res/values/strings.xml new file mode 100644 index 000000000000..3a8aaf8b5092 --- /dev/null +++ b/packages/SettingsLib/IllustrationPreference/res/values/strings.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2025 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Label for an accessibility action that starts an animation [CHAR LIMIT=30] --> + <string name="settingslib_action_label_resume">resume</string> + <!-- Label for an accessibility action that stops an animation [CHAR LIMIT=30] --> + <string name="settingslib_action_label_pause">pause</string> + <!-- Label for an accessibility action that stops an animation [CHAR LIMIT=50] --> + <string name="settingslib_state_animation_playing">Animation playing</string> + <!-- Label for an accessibility action that stops an animation [CHAR LIMIT=50] --> + <string name="settingslib_state_animation_paused">Animation paused</string> +</resources> diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java index af40c647e805..e818a603c5b4 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -32,6 +32,8 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.FrameLayout; import android.widget.ImageView; @@ -73,6 +75,7 @@ public class IllustrationPreference extends Preference implements GroupSectionDi private boolean mLottieDynamicColor; private CharSequence mContentDescription; private boolean mIsTablet; + private boolean mIsAnimatable; private boolean mIsAnimationPaused; /** @@ -81,6 +84,7 @@ public class IllustrationPreference extends Preference implements GroupSectionDi public interface OnBindListener { /** * Called when when {@link #onBindViewHolder(PreferenceViewHolder)} occurs. + * * @param animationView the animation view for this preference. */ void onBind(LottieAnimationView animationView); @@ -144,16 +148,6 @@ public class IllustrationPreference extends Preference implements GroupSectionDi (FrameLayout) holder.findViewById(R.id.middleground_layout); final LottieAnimationView illustrationView = (LottieAnimationView) holder.findViewById(R.id.lottie_view); - // Pause and resume animation - illustrationFrame.setOnClickListener(v -> { - mIsAnimationPaused = !mIsAnimationPaused; - if (mIsAnimationPaused) { - illustrationView.pauseAnimation(); - } else { - illustrationView.resumeAnimation(); - } - }); - if (illustrationView != null && !TextUtils.isEmpty(mContentDescription)) { illustrationView.setContentDescription(mContentDescription); illustrationView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); @@ -171,12 +165,13 @@ public class IllustrationPreference extends Preference implements GroupSectionDi illustrationView.setCacheComposition(mCacheComposition); handleImageWithAnimation(illustrationView, illustrationFrame); + handleAnimationControl(illustrationView, illustrationFrame); handleImageFrameMaxHeight(backgroundView, illustrationView); if (mIsAutoScale) { illustrationView.setScaleType(mIsAutoScale - ? ImageView.ScaleType.CENTER_CROP - : ImageView.ScaleType.CENTER_INSIDE); + ? ImageView.ScaleType.CENTER_CROP + : ImageView.ScaleType.CENTER_INSIDE); } handleMiddleGroundView(middleGroundLayout); @@ -377,6 +372,7 @@ public class IllustrationPreference extends Preference implements GroupSectionDi final Drawable drawable = illustrationView.getDrawable(); if (drawable != null) { startAnimation(drawable); + mIsAnimatable = false; } } @@ -386,10 +382,12 @@ public class IllustrationPreference extends Preference implements GroupSectionDi final Drawable drawable = illustrationView.getDrawable(); if (drawable != null) { startAnimation(drawable); + mIsAnimatable = false; } else { // The lottie image from the raw folder also returns null because the ImageView // couldn't handle it now. startLottieAnimationWith(illustrationView, mImageUri); + mIsAnimatable = true; } } @@ -418,10 +416,12 @@ public class IllustrationPreference extends Preference implements GroupSectionDi final Drawable drawable = illustrationView.getDrawable(); if (drawable != null) { startAnimation(drawable); + mIsAnimatable = false; } else { // The lottie image from the raw folder also returns null because the ImageView // couldn't handle it now. startLottieAnimationWith(illustrationView, mImageResId); + mIsAnimatable = true; } } } @@ -459,6 +459,60 @@ public class IllustrationPreference extends Preference implements GroupSectionDi ((Animatable) drawable).start(); } + private void handleAnimationControl(LottieAnimationView illustrationView, + ViewGroup container) { + if (mIsAnimatable) { + // TODO(b/397340540): list out pages having illustration without a content description. + if (TextUtils.isEmpty(mContentDescription)) { + Log.w(TAG, "Illustration should have a content description. preference key = " + + getKey()); + } + // Enable pause and resume abilities to animation only + container.setOnClickListener(v -> { + mIsAnimationPaused = !mIsAnimationPaused; + if (mIsAnimationPaused) { + illustrationView.pauseAnimation(); + } else { + illustrationView.resumeAnimation(); + } + updateAccessibilityAction(container); + }); + + updateAccessibilityAction(container); + } + } + + private void updateAccessibilityAction(ViewGroup container) { + // Setting the state of animation + container.setStateDescription(getStateDescriptionForAnimation()); + container.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final AccessibilityAction clickAction = new AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, + getActionLabelForAnimation()); + info.addAction(clickAction); + } + }); + } + + private String getActionLabelForAnimation() { + if (mIsAnimationPaused) { + return getContext().getString(R.string.settingslib_action_label_resume); + } else { + return getContext().getString(R.string.settingslib_action_label_pause); + } + } + + private String getStateDescriptionForAnimation() { + if (mIsAnimationPaused) { + return getContext().getString(R.string.settingslib_state_animation_paused); + } else { + return getContext().getString(R.string.settingslib_state_animation_playing); + } + } + private static void startLottieAnimationWith(LottieAnimationView illustrationView, Uri imageUri) { final InputStream inputStream = @@ -514,15 +568,20 @@ public class IllustrationPreference extends Preference implements GroupSectionDi mIsAutoScale = false; if (attrs != null) { TypedArray a = context.obtainStyledAttributes(attrs, - com.airbnb.lottie.R.styleable.LottieAnimationView, 0 /*defStyleAttr*/, 0 /*defStyleRes*/); - mImageResId = a.getResourceId(com.airbnb.lottie.R.styleable.LottieAnimationView_lottie_rawRes, 0); + com.airbnb.lottie.R.styleable.LottieAnimationView, /* defStyleAttr= */ 0, + /* defStyleRes= */ 0); + mImageResId = a.getResourceId( + com.airbnb.lottie.R.styleable.LottieAnimationView_lottie_rawRes, + /* defValue= */ 0); mCacheComposition = a.getBoolean( - com.airbnb.lottie.R.styleable.LottieAnimationView_lottie_cacheComposition, true); + com.airbnb.lottie.R.styleable.LottieAnimationView_lottie_cacheComposition, + /* defValue= */ true); a = context.obtainStyledAttributes(attrs, - R.styleable.IllustrationPreference, 0 /*defStyleAttr*/, 0 /*defStyleRes*/); + R.styleable.IllustrationPreference, /* defStyleAttr= */ 0, + /* defStyleRes= */ 0); mLottieDynamicColor = a.getBoolean(R.styleable.IllustrationPreference_dynamicColor, - false); + /* defValue= */ false); a.recycle(); } diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml index dc5c9b297181..b6e80c784f10 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/themes.xml @@ -18,7 +18,7 @@ <resources> <style name="Theme.SettingsBase_v35" parent="Theme.SettingsBase_v33" > <item name="android:colorAccent">@color/settingslib_materialColorPrimary</item> - <item name="android:colorBackground">@color/settingslib_materialColorSurfaceContainerLowest</item> + <item name="android:colorBackground">@color/settingslib_materialColorSurfaceContainer</item> <item name="android:textColorPrimary">@color/settingslib_materialColorOnSurface</item> <item name="android:textColorSecondary">@color/settingslib_text_color_secondary</item> <item name="android:textColorTertiary">@color/settingslib_materialColorOutline</item> diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt index 6a0632073de9..a04fce7eeb86 100644 --- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt +++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt @@ -47,7 +47,7 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) : private val mHandler = Handler(Looper.getMainLooper()) - private val syncRunnable = Runnable { updatePreferences() } + private val syncRunnable = Runnable { updatePreferencesList() } init { val context = preferenceGroup.context @@ -64,7 +64,7 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) : true, /* resolveRefs */ ) mLegacyBackgroundRes = outValue.resourceId - updatePreferences() + updatePreferencesList() } @SuppressLint("RestrictedApi") @@ -82,7 +82,7 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) : updateBackground(holder, position) } - private fun updatePreferences() { + private fun updatePreferencesList() { val oldList = ArrayList(mRoundCornerMappingList) mRoundCornerMappingList = ArrayList() mappingPreferenceGroup(mRoundCornerMappingList, mPreferenceGroup) diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsThemeHelper.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsThemeHelper.kt index 1776d252e28b..1f4a48df55b0 100644 --- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsThemeHelper.kt +++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsThemeHelper.kt @@ -50,10 +50,6 @@ object SettingsThemeHelper { } private fun tryInit(context: Context) { - if (expressiveThemeState != ExpressiveThemeState.UNKNOWN) { - return - } - expressiveThemeState = if ( (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) && diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index d94450b1cabd..a029f56cf1d7 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -26,14 +26,14 @@ flag { name: "enable_le_audio_sharing" namespace: "pixel_cross_device_control" description: "Gates whether to enable LE audio sharing" - bug: "323125723" + bug: "388674074" } flag { name: "enable_le_audio_qr_code_private_broadcast_sharing" namespace: "pixel_cross_device_control" description: "Gates whether to enable LE audio private broadcast sharing via QR code" - bug: "323125723" + bug: "388674074" } flag { @@ -229,3 +229,23 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "adopt_primary_group_management_api_v2" + namespace: "cross_device_experiences" + description: "Adopt more Bluetooth LE broadcast primary group management APIs post launch" + bug: "381946931" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "promote_audio_sharing_for_second_auto_connected_lea_device" + namespace: "cross_device_experiences" + description: "Show audio sharing promote notification or dialog when the second lea device is auto connected" + bug: "395786392" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml index 6015be8c894f..aa5e9d28cabe 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml @@ -23,10 +23,10 @@ <clip-path android:pathData=" M0,0 - V13.5,0 - H13.5,20 - V0,20 - H0,0 + H16 + V12 + H0 + Z M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> <path android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml index fb73b6b253e1..f7bf8518a55c 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml @@ -8,10 +8,10 @@ <clip-path android:pathData=" M0,0 - V13.5,0 - H13.5,20 - V0,20 - H0,0 + H16 + V12 + H0 + Z M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> <path android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml index 7c4c1c6b1126..016c614846e9 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml @@ -23,10 +23,10 @@ <clip-path android:pathData=" M0,0 - V13.5,0 - H13.5,20 - V0,20 - H0,0 + H16 + V12 + H0 + Z M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> <path android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml index d23680d17b9c..b8304151eefe 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml @@ -23,10 +23,10 @@ <clip-path android:pathData=" M0,0 - V13.5,0 - H13.5,20 - V0,20 - H0,0 + H16 + V12 + H0 + Z M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> <path android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" diff --git a/packages/SettingsLib/res/drawable/ic_wifi_0.xml b/packages/SettingsLib/res/drawable/ic_wifi_0.xml index 8ff65540c505..7f98457a4c76 100644 --- a/packages/SettingsLib/res/drawable/ic_wifi_0.xml +++ b/packages/SettingsLib/res/drawable/ic_wifi_0.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 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,24 +14,24 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="18dp" - android:height="13dp" - android:viewportWidth="18.0" - android:viewportHeight="13.0"> - <path - android:pathData="M0.523,3.314C0.32,3.502 0.32,3.819 0.516,4.015L1.223,4.722C1.418,4.917 1.734,4.916 1.938,4.73C5.936,1.09 12.066,1.09 16.064,4.73C16.268,4.916 16.584,4.917 16.779,4.722L17.486,4.015C17.682,3.819 17.682,3.502 17.479,3.314C12.698,-1.105 5.304,-1.105 0.523,3.314Z" - android:fillAlpha="0.24" - android:fillColor="#000"/> + android:width="17.0dp" + android:height="12.58dp" + android:viewportHeight="12.0" + android:viewportWidth="16.21"> + <path - android:pathData="M15.011,6.49C15.207,6.294 15.207,5.976 15.002,5.792C11.592,2.736 6.411,2.736 3,5.792C2.795,5.976 2.795,6.294 2.991,6.49L3.698,7.197C3.893,7.392 4.209,7.39 4.417,7.209C7.042,4.93 10.96,4.93 13.585,7.209C13.793,7.39 14.109,7.392 14.304,7.197L15.011,6.49Z" - android:fillAlpha="0.24" - android:fillColor="#000"/> + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M12.659,7.45C13.199,6.91 13.149,6.01 12.479,5.64C11.179,4.92 9.689,4.51 8.109,4.51C6.529,4.51 5.029,4.92 3.739,5.64C3.069,6.01 3.019,6.91 3.559,7.45C3.999,7.89 4.699,7.94 5.259,7.66C6.119,7.24 7.089,7 8.109,7C9.129,7 10.099,7.24 10.959,7.66C11.519,7.94 12.219,7.89 12.659,7.45Z" /> + <path - android:pathData="M5.465,8.964C5.27,8.769 5.269,8.45 5.481,8.273C7.515,6.576 10.487,6.576 12.521,8.273C12.733,8.45 12.732,8.769 12.537,8.964L11.83,9.672C11.634,9.867 11.319,9.863 11.099,9.698C9.859,8.767 8.143,8.767 6.904,9.698C6.683,9.863 6.368,9.867 6.173,9.672L5.465,8.964Z" - android:fillAlpha="0.24" - android:fillColor="#000"/> + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M15.85,4.26C16.36,3.75 16.34,2.91 15.76,2.49C13.61,0.93 10.97,0 8.11,0C5.25,0 2.61,0.92 0.46,2.49C-0.12,2.91 -0.14,3.75 0.37,4.26C0.84,4.73 1.59,4.75 2.13,4.37C3.83,3.19 5.89,2.5 8.11,2.5C10.33,2.5 12.39,3.19 14.09,4.37C14.63,4.75 15.38,4.73 15.85,4.26Z" /> + <path - android:pathData="M10.062,11.439C10.257,11.244 10.259,10.92 10.022,10.779C9.395,10.407 8.608,10.407 7.98,10.779C7.743,10.92 7.745,11.244 7.94,11.439L8.647,12.146C8.843,12.342 9.159,12.342 9.355,12.146L10.062,11.439Z" - android:fillAlpha="0.24" - android:fillColor="#000"/> + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M8.109,12C8.938,12 9.609,11.328 9.609,10.5C9.609,9.672 8.938,9 8.109,9C7.281,9 6.609,9.672 6.609,10.5C6.609,11.328 7.281,12 8.109,12Z" /> + </vector> diff --git a/packages/SettingsLib/res/drawable/ic_wifi_0_error.xml b/packages/SettingsLib/res/drawable/ic_wifi_0_error.xml index db31b9dd0fcd..3a9bba5cd526 100644 --- a/packages/SettingsLib/res/drawable/ic_wifi_0_error.xml +++ b/packages/SettingsLib/res/drawable/ic_wifi_0_error.xml @@ -1,28 +1,52 @@ +<!-- + Copyright (C) 2025 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. +--> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="17dp" - android:height="13dp" - android:viewportWidth="17.0" - android:viewportHeight="13.0"> + android:width="21.0dp" + android:height="15.66dp" + android:viewportHeight="13.5" + android:viewportWidth="18.1"> + + <group> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <clip-path android:pathData=" + M0,0 + H18.1 + V13.5 + H0 + Z + M15.109,13.5C17.871,13.5 20.109,11.261 20.109,8.5C20.109,5.739 17.871,3.5 15.109,3.5C12.348,3.5 10.109,5.739 10.109,8.5C10.109,11.261 12.348,13.5 15.109,13.5Z" /> + + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M8.109,12C8.938,12 9.609,11.328 9.609,10.5C9.609,9.672 8.938,9 8.109,9C7.281,9 6.609,9.672 6.609,10.5C6.609,11.328 7.281,12 8.109,12Z" /> + + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M12.659,7.45C13.199,6.91 13.149,6.01 12.479,5.64C11.179,4.92 9.689,4.51 8.109,4.51C6.529,4.51 5.029,4.92 3.739,5.64C3.069,6.01 3.019,6.91 3.559,7.45C3.999,7.89 4.699,7.94 5.259,7.66C6.119,7.24 7.089,7 8.109,7C9.129,7 10.099,7.24 10.959,7.66C11.519,7.94 12.219,7.89 12.659,7.45Z" /> + + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M15.85,4.26C16.36,3.75 16.34,2.91 15.76,2.49C13.61,0.93 10.97,0 8.11,0C5.25,0 2.61,0.92 0.46,2.49C-0.12,2.91 -0.14,3.75 0.37,4.26C0.84,4.73 1.59,4.75 2.13,4.37C3.83,3.19 5.89,2.5 8.11,2.5C10.33,2.5 12.39,3.19 14.09,4.37C14.63,4.75 15.38,4.73 15.85,4.26Z" /> + </group> + <path - android:pathData="M0.146,4.015C-0.05,3.819 -0.05,3.502 0.153,3.314C4.002,-0.244 9.545,-0.937 14.055,1.234C13.339,1.449 12.792,2.053 12.66,2.801C8.998,1.281 4.65,1.924 1.568,4.73C1.364,4.916 1.048,4.917 0.853,4.722L0.146,4.015Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M12.63,4.435C9.406,2.836 5.424,3.288 2.63,5.792C2.424,5.976 2.425,6.294 2.621,6.49L3.328,7.197C3.523,7.392 3.839,7.39 4.047,7.209C6.484,5.094 10.033,4.942 12.63,6.753V4.435Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M5.095,8.964C4.9,8.769 4.899,8.45 5.111,8.273C7.145,6.576 10.117,6.576 12.151,8.273C12.363,8.45 12.362,8.769 12.166,8.964L11.459,9.672C11.264,9.867 10.949,9.863 10.728,9.698C9.489,8.767 7.773,8.767 6.533,9.698C6.313,9.863 5.998,9.867 5.802,9.672L5.095,8.964Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M9.652,10.779C9.889,10.92 9.887,11.244 9.692,11.439L8.984,12.146C8.789,12.342 8.473,12.342 8.277,12.146L7.57,11.439C7.375,11.244 7.373,10.92 7.61,10.779C8.237,10.407 9.024,10.407 9.652,10.779Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M14.63,11.15C14.63,10.598 15.078,10.15 15.63,10.15C16.182,10.15 16.63,10.598 16.63,11.15C16.63,11.703 16.182,12.15 15.63,12.15C15.078,12.15 14.63,11.703 14.63,11.15Z" - android:fillColor="#000"/> - <path - android:pathData="M14.63,3.15C14.63,2.874 14.854,2.65 15.13,2.65H16.13C16.406,2.65 16.63,2.874 16.63,3.15V8.15C16.63,8.427 16.406,8.65 16.13,8.65H15.13C14.854,8.65 14.63,8.427 14.63,8.15V3.15Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M15.109,5C14.699,5 14.359,5.34 14.359,5.75L14.359,8.75C14.359,9.16 14.699,9.5 15.109,9.5C15.519,9.5 15.859,9.16 15.859,8.75L15.859,5.75C15.859,5.34 15.519,5 15.109,5ZM15.109,12C15.519,12 15.859,11.66 15.859,11.25C15.859,10.84 15.519,10.5 15.109,10.5C14.699,10.5 14.359,10.84 14.359,11.25C14.359,11.66 14.699,12 15.109,12Z" /> + </vector> diff --git a/packages/SettingsLib/res/drawable/ic_wifi_1.xml b/packages/SettingsLib/res/drawable/ic_wifi_1.xml index e170f1dadc94..9c661f4b5d7a 100644 --- a/packages/SettingsLib/res/drawable/ic_wifi_1.xml +++ b/packages/SettingsLib/res/drawable/ic_wifi_1.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 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,23 +14,23 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="18dp" - android:height="13dp" - android:viewportWidth="18.0" - android:viewportHeight="13.0"> - <path - android:pathData="M0.523,3.314C0.32,3.502 0.32,3.819 0.516,4.015L1.223,4.722C1.418,4.917 1.734,4.916 1.938,4.73C5.936,1.09 12.066,1.09 16.064,4.73C16.268,4.916 16.584,4.917 16.779,4.722L17.486,4.015C17.682,3.819 17.682,3.502 17.479,3.314C12.698,-1.105 5.304,-1.105 0.523,3.314Z" - android:fillAlpha="0.24" - android:fillColor="#000"/> + android:width="17.0dp" + android:height="12.58dp" + android:viewportHeight="12.0" + android:viewportWidth="16.21"> + <path - android:pathData="M15.011,6.49C15.207,6.294 15.207,5.976 15.002,5.792C11.592,2.736 6.411,2.736 3,5.792C2.795,5.976 2.795,6.294 2.991,6.49L3.698,7.197C3.893,7.392 4.209,7.39 4.417,7.209C7.042,4.93 10.96,4.93 13.585,7.209C13.793,7.39 14.109,7.392 14.304,7.197L15.011,6.49Z" - android:fillAlpha="0.24" - android:fillColor="#000"/> + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M12.659,7.45C13.199,6.91 13.149,6.01 12.479,5.64C11.179,4.92 9.689,4.51 8.109,4.51C6.529,4.51 5.029,4.92 3.739,5.64C3.069,6.01 3.019,6.91 3.559,7.45C3.999,7.89 4.699,7.94 5.259,7.66C6.119,7.24 7.089,7 8.109,7C9.129,7 10.099,7.24 10.959,7.66C11.519,7.94 12.219,7.89 12.659,7.45Z" /> + <path - android:pathData="M5.465,8.964C5.27,8.769 5.269,8.45 5.481,8.273C7.515,6.576 10.487,6.576 12.521,8.273C12.733,8.45 12.732,8.769 12.537,8.964L11.83,9.672C11.634,9.867 11.319,9.863 11.099,9.698C9.859,8.767 8.143,8.767 6.904,9.698C6.683,9.863 6.368,9.867 6.173,9.672L5.465,8.964Z" - android:fillAlpha="0.24" - android:fillColor="#000"/> + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M15.85,4.26C16.36,3.75 16.34,2.91 15.76,2.49C13.61,0.93 10.97,0 8.11,0C5.25,0 2.61,0.92 0.46,2.49C-0.12,2.91 -0.14,3.75 0.37,4.26C0.84,4.73 1.59,4.75 2.13,4.37C3.83,3.19 5.89,2.5 8.11,2.5C10.33,2.5 12.39,3.19 14.09,4.37C14.63,4.75 15.38,4.73 15.85,4.26Z" /> + <path - android:pathData="M10.062,11.439C10.257,11.244 10.259,10.92 10.022,10.779C9.395,10.407 8.608,10.407 7.98,10.779C7.743,10.92 7.745,11.244 7.94,11.439L8.647,12.146C8.843,12.342 9.159,12.342 9.355,12.146L10.062,11.439Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M8.109,12C8.938,12 9.609,11.328 9.609,10.5C9.609,9.672 8.938,9 8.109,9C7.281,9 6.609,9.672 6.609,10.5C6.609,11.328 7.281,12 8.109,12Z" /> + </vector> diff --git a/packages/SettingsLib/res/drawable/ic_wifi_1_error.xml b/packages/SettingsLib/res/drawable/ic_wifi_1_error.xml index a4d6a5c00b15..32a69f5e810c 100644 --- a/packages/SettingsLib/res/drawable/ic_wifi_1_error.xml +++ b/packages/SettingsLib/res/drawable/ic_wifi_1_error.xml @@ -1,27 +1,51 @@ +<!-- + Copyright (C) 2025 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. +--> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="17dp" - android:height="13dp" - android:viewportWidth="17.0" - android:viewportHeight="13.0"> + android:width="21.0dp" + android:height="15.66dp" + android:viewportHeight="13.5" + android:viewportWidth="18.1"> + + <group> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <clip-path android:pathData=" + M0,0 + H18.1 + V13.5 + H0 + Z + M15.109,13.5C17.871,13.5 20.109,11.261 20.109,8.5C20.109,5.739 17.871,3.5 15.109,3.5C12.348,3.5 10.109,5.739 10.109,8.5C10.109,11.261 12.348,13.5 15.109,13.5Z" /> + + <path + android:fillColor="#000" + android:pathData="M8.109,12C8.938,12 9.609,11.328 9.609,10.5C9.609,9.672 8.938,9 8.109,9C7.281,9 6.609,9.672 6.609,10.5C6.609,11.328 7.281,12 8.109,12Z" /> + + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M12.659,7.45C13.199,6.91 13.149,6.01 12.479,5.64C11.179,4.92 9.689,4.51 8.109,4.51C6.529,4.51 5.029,4.92 3.739,5.64C3.069,6.01 3.019,6.91 3.559,7.45C3.999,7.89 4.699,7.94 5.259,7.66C6.119,7.24 7.089,7 8.109,7C9.129,7 10.099,7.24 10.959,7.66C11.519,7.94 12.219,7.89 12.659,7.45Z" /> + + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M15.85,4.26C16.36,3.75 16.34,2.91 15.76,2.49C13.61,0.93 10.97,0 8.11,0C5.25,0 2.61,0.92 0.46,2.49C-0.12,2.91 -0.14,3.75 0.37,4.26C0.84,4.73 1.59,4.75 2.13,4.37C3.83,3.19 5.89,2.5 8.11,2.5C10.33,2.5 12.39,3.19 14.09,4.37C14.63,4.75 15.38,4.73 15.85,4.26Z" /> + </group> + <path - android:pathData="M0.146,4.015C-0.05,3.819 -0.05,3.502 0.153,3.314C4.002,-0.244 9.545,-0.937 14.055,1.234C13.339,1.449 12.792,2.053 12.66,2.801C8.998,1.281 4.65,1.924 1.568,4.73C1.364,4.916 1.048,4.917 0.853,4.722L0.146,4.015Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M12.63,4.435C9.406,2.836 5.424,3.288 2.63,5.792C2.424,5.976 2.425,6.294 2.621,6.49L3.328,7.197C3.523,7.392 3.839,7.39 4.047,7.209C6.484,5.094 10.033,4.942 12.63,6.753V4.435Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M5.095,8.964C4.9,8.769 4.899,8.45 5.111,8.273C7.145,6.576 10.117,6.576 12.151,8.273C12.363,8.45 12.362,8.769 12.166,8.964L11.459,9.672C11.264,9.867 10.949,9.863 10.728,9.698C9.489,8.767 7.773,8.767 6.533,9.698C6.313,9.863 5.998,9.867 5.802,9.672L5.095,8.964Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M9.652,10.779C9.889,10.92 9.887,11.244 9.692,11.439L8.984,12.146C8.789,12.342 8.473,12.342 8.277,12.146L7.57,11.439C7.375,11.244 7.373,10.92 7.61,10.779C8.237,10.407 9.024,10.407 9.652,10.779Z" - android:fillColor="#000"/> - <path - android:pathData="M14.63,11.15C14.63,10.598 15.078,10.15 15.63,10.15C16.182,10.15 16.63,10.598 16.63,11.15C16.63,11.703 16.182,12.15 15.63,12.15C15.078,12.15 14.63,11.703 14.63,11.15Z" - android:fillColor="#000"/> - <path - android:pathData="M14.63,3.15C14.63,2.874 14.854,2.65 15.13,2.65H16.13C16.406,2.65 16.63,2.874 16.63,3.15V8.15C16.63,8.427 16.406,8.65 16.13,8.65H15.13C14.854,8.65 14.63,8.427 14.63,8.15V3.15Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M15.109,5C14.699,5 14.359,5.34 14.359,5.75L14.359,8.75C14.359,9.16 14.699,9.5 15.109,9.5C15.519,9.5 15.859,9.16 15.859,8.75L15.859,5.75C15.859,5.34 15.519,5 15.109,5ZM15.109,12C15.519,12 15.859,11.66 15.859,11.25C15.859,10.84 15.519,10.5 15.109,10.5C14.699,10.5 14.359,10.84 14.359,11.25C14.359,11.66 14.699,12 15.109,12Z" /> + </vector> diff --git a/packages/SettingsLib/res/drawable/ic_wifi_2.xml b/packages/SettingsLib/res/drawable/ic_wifi_2.xml index fc62267ad5b0..02c14e18a95a 100644 --- a/packages/SettingsLib/res/drawable/ic_wifi_2.xml +++ b/packages/SettingsLib/res/drawable/ic_wifi_2.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 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,22 +14,22 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="18dp" - android:height="13dp" - android:viewportWidth="18.0" - android:viewportHeight="13.0"> - <path - android:pathData="M0.523,3.314C0.32,3.502 0.32,3.819 0.516,4.015L1.223,4.722C1.418,4.917 1.734,4.916 1.938,4.73C5.936,1.09 12.066,1.09 16.064,4.73C16.268,4.916 16.584,4.917 16.779,4.722L17.486,4.015C17.682,3.819 17.682,3.502 17.479,3.314C12.698,-1.105 5.304,-1.105 0.523,3.314Z" - android:fillAlpha="0.24" - android:fillColor="#000"/> + android:width="17.0dp" + android:height="12.58dp" + android:viewportHeight="12.0" + android:viewportWidth="16.21"> + <path - android:pathData="M15.011,6.49C15.207,6.294 15.207,5.976 15.002,5.792C11.592,2.736 6.411,2.736 3,5.792C2.795,5.976 2.795,6.294 2.991,6.49L3.698,7.197C3.893,7.392 4.209,7.39 4.417,7.209C7.042,4.93 10.96,4.93 13.585,7.209C13.793,7.39 14.109,7.392 14.304,7.197L15.011,6.49Z" - android:fillAlpha="0.24" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M12.659,7.45C13.199,6.91 13.149,6.01 12.479,5.64C11.179,4.92 9.689,4.51 8.109,4.51C6.529,4.51 5.029,4.92 3.739,5.64C3.069,6.01 3.019,6.91 3.559,7.45C3.999,7.89 4.699,7.94 5.259,7.66C6.119,7.24 7.089,7 8.109,7C9.129,7 10.099,7.24 10.959,7.66C11.519,7.94 12.219,7.89 12.659,7.45Z" /> + <path - android:pathData="M5.465,8.964C5.27,8.769 5.269,8.45 5.481,8.273C7.515,6.576 10.487,6.576 12.521,8.273C12.733,8.45 12.732,8.769 12.537,8.964L11.83,9.672C11.634,9.867 11.319,9.863 11.099,9.698C9.859,8.767 8.143,8.767 6.904,9.698C6.683,9.863 6.368,9.867 6.173,9.672L5.465,8.964Z" - android:fillColor="#000"/> + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M15.85,4.26C16.36,3.75 16.34,2.91 15.76,2.49C13.61,0.93 10.97,0 8.11,0C5.25,0 2.61,0.92 0.46,2.49C-0.12,2.91 -0.14,3.75 0.37,4.26C0.84,4.73 1.59,4.75 2.13,4.37C3.83,3.19 5.89,2.5 8.11,2.5C10.33,2.5 12.39,3.19 14.09,4.37C14.63,4.75 15.38,4.73 15.85,4.26Z" /> + <path - android:pathData="M10.062,11.439C10.257,11.244 10.259,10.92 10.022,10.779C9.395,10.407 8.608,10.407 7.98,10.779C7.743,10.92 7.745,11.244 7.94,11.439L8.647,12.146C8.843,12.342 9.159,12.342 9.355,12.146L10.062,11.439Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M8.109,12C8.938,12 9.609,11.328 9.609,10.5C9.609,9.672 8.938,9 8.109,9C7.281,9 6.609,9.672 6.609,10.5C6.609,11.328 7.281,12 8.109,12Z" /> + </vector> diff --git a/packages/SettingsLib/res/drawable/ic_wifi_2_error.xml b/packages/SettingsLib/res/drawable/ic_wifi_2_error.xml index 65f40eff1ca8..da0aa12de564 100644 --- a/packages/SettingsLib/res/drawable/ic_wifi_2_error.xml +++ b/packages/SettingsLib/res/drawable/ic_wifi_2_error.xml @@ -1,26 +1,50 @@ +<!-- + Copyright (C) 2025 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. +--> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="17dp" - android:height="13dp" - android:viewportWidth="17.0" - android:viewportHeight="13.0"> + android:width="21.0dp" + android:height="15.66dp" + android:viewportHeight="13.5" + android:viewportWidth="18.1"> + + <group> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <clip-path android:pathData=" + M0,0 + H18.1 + V13.5 + H0 + Z + M15.109,13.5C17.871,13.5 20.109,11.261 20.109,8.5C20.109,5.739 17.871,3.5 15.109,3.5C12.348,3.5 10.109,5.739 10.109,8.5C10.109,11.261 12.348,13.5 15.109,13.5Z" /> + + <path + android:fillColor="#000" + android:pathData="M8.109,12C8.938,12 9.609,11.328 9.609,10.5C9.609,9.672 8.938,9 8.109,9C7.281,9 6.609,9.672 6.609,10.5C6.609,11.328 7.281,12 8.109,12Z" /> + + <path + android:fillColor="#000" + android:pathData="M12.659,7.45C13.199,6.91 13.149,6.01 12.479,5.64C11.179,4.92 9.689,4.51 8.109,4.51C6.529,4.51 5.029,4.92 3.739,5.64C3.069,6.01 3.019,6.91 3.559,7.45C3.999,7.89 4.699,7.94 5.259,7.66C6.119,7.24 7.089,7 8.109,7C9.129,7 10.099,7.24 10.959,7.66C11.519,7.94 12.219,7.89 12.659,7.45Z" /> + + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M15.85,4.26C16.36,3.75 16.34,2.91 15.76,2.49C13.61,0.93 10.97,0 8.11,0C5.25,0 2.61,0.92 0.46,2.49C-0.12,2.91 -0.14,3.75 0.37,4.26C0.84,4.73 1.59,4.75 2.13,4.37C3.83,3.19 5.89,2.5 8.11,2.5C10.33,2.5 12.39,3.19 14.09,4.37C14.63,4.75 15.38,4.73 15.85,4.26Z" /> + </group> + <path - android:pathData="M0.146,4.015C-0.05,3.819 -0.05,3.502 0.153,3.314C4.002,-0.244 9.545,-0.937 14.055,1.234C13.339,1.449 12.792,2.053 12.66,2.801C8.998,1.281 4.65,1.924 1.568,4.73C1.364,4.916 1.048,4.917 0.853,4.722L0.146,4.015Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M12.63,4.435C9.406,2.836 5.424,3.288 2.63,5.792C2.424,5.976 2.425,6.294 2.621,6.49L3.328,7.197C3.523,7.392 3.839,7.39 4.047,7.209C6.484,5.094 10.033,4.942 12.63,6.753V4.435Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M5.095,8.964C4.9,8.769 4.899,8.45 5.111,8.273C7.145,6.576 10.117,6.576 12.151,8.273C12.363,8.45 12.362,8.769 12.166,8.964L11.459,9.672C11.264,9.867 10.949,9.863 10.728,9.698C9.489,8.767 7.773,8.767 6.533,9.698C6.313,9.863 5.998,9.867 5.802,9.672L5.095,8.964Z" - android:fillColor="#000"/> - <path - android:pathData="M9.652,10.779C9.889,10.92 9.887,11.244 9.692,11.439L8.984,12.146C8.789,12.342 8.473,12.342 8.277,12.146L7.57,11.439C7.375,11.244 7.373,10.92 7.61,10.779C8.237,10.407 9.024,10.407 9.652,10.779Z" - android:fillColor="#000"/> - <path - android:pathData="M14.63,11.15C14.63,10.598 15.078,10.15 15.63,10.15C16.182,10.15 16.63,10.598 16.63,11.15C16.63,11.703 16.182,12.15 15.63,12.15C15.078,12.15 14.63,11.703 14.63,11.15Z" - android:fillColor="#000"/> - <path - android:pathData="M14.63,3.15C14.63,2.874 14.854,2.65 15.13,2.65H16.13C16.406,2.65 16.63,2.874 16.63,3.15V8.15C16.63,8.427 16.406,8.65 16.13,8.65H15.13C14.854,8.65 14.63,8.427 14.63,8.15V3.15Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M15.109,5C14.699,5 14.359,5.34 14.359,5.75L14.359,8.75C14.359,9.16 14.699,9.5 15.109,9.5C15.519,9.5 15.859,9.16 15.859,8.75L15.859,5.75C15.859,5.34 15.519,5 15.109,5ZM15.109,12C15.519,12 15.859,11.66 15.859,11.25C15.859,10.84 15.519,10.5 15.109,10.5C14.699,10.5 14.359,10.84 14.359,11.25C14.359,11.66 14.699,12 15.109,12Z" /> + </vector> diff --git a/packages/SettingsLib/res/drawable/ic_wifi_3.xml b/packages/SettingsLib/res/drawable/ic_wifi_3.xml index 9079daf922b8..6b183aff40f0 100644 --- a/packages/SettingsLib/res/drawable/ic_wifi_3.xml +++ b/packages/SettingsLib/res/drawable/ic_wifi_3.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 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,21 +14,21 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="18dp" - android:height="13dp" - android:viewportWidth="18.0" - android:viewportHeight="13.0"> - <path - android:pathData="M0.523,3.314C0.32,3.502 0.32,3.819 0.516,4.015L1.223,4.722C1.418,4.917 1.734,4.916 1.938,4.73C5.936,1.09 12.066,1.09 16.064,4.73C16.268,4.916 16.584,4.917 16.779,4.722L17.486,4.015C17.682,3.819 17.682,3.502 17.479,3.314C12.698,-1.105 5.304,-1.105 0.523,3.314Z" - android:fillAlpha="0.24" - android:fillColor="#000"/> + android:width="17.0dp" + android:height="12.58dp" + android:viewportHeight="12.0" + android:viewportWidth="16.21"> + <path - android:pathData="M15.011,6.49C15.207,6.294 15.207,5.976 15.002,5.792C11.592,2.736 6.411,2.736 3,5.792C2.795,5.976 2.795,6.294 2.991,6.49L3.698,7.197C3.893,7.392 4.209,7.39 4.417,7.209C7.042,4.93 10.96,4.93 13.585,7.209C13.793,7.39 14.109,7.392 14.304,7.197L15.011,6.49Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M12.659,7.45C13.199,6.91 13.149,6.01 12.479,5.64C11.179,4.92 9.689,4.51 8.109,4.51C6.529,4.51 5.029,4.92 3.739,5.64C3.069,6.01 3.019,6.91 3.559,7.45C3.999,7.89 4.699,7.94 5.259,7.66C6.119,7.24 7.089,7 8.109,7C9.129,7 10.099,7.24 10.959,7.66C11.519,7.94 12.219,7.89 12.659,7.45Z" /> + <path - android:pathData="M5.465,8.964C5.27,8.769 5.269,8.45 5.481,8.273C7.515,6.576 10.487,6.576 12.521,8.273C12.733,8.45 12.732,8.769 12.537,8.964L11.83,9.672C11.634,9.867 11.319,9.863 11.099,9.698C9.859,8.767 8.143,8.767 6.904,9.698C6.683,9.863 6.368,9.867 6.173,9.672L5.465,8.964Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M15.85,4.26C16.36,3.75 16.34,2.91 15.76,2.49C13.61,0.93 10.97,0 8.11,0C5.25,0 2.61,0.92 0.46,2.49C-0.12,2.91 -0.14,3.75 0.37,4.26C0.84,4.73 1.59,4.75 2.13,4.37C3.83,3.19 5.89,2.5 8.11,2.5C10.33,2.5 12.39,3.19 14.09,4.37C14.63,4.75 15.38,4.73 15.85,4.26Z" /> + <path - android:pathData="M10.062,11.439C10.257,11.244 10.259,10.92 10.022,10.779C9.395,10.407 8.608,10.407 7.98,10.779C7.743,10.92 7.745,11.244 7.94,11.439L8.647,12.146C8.843,12.342 9.159,12.342 9.355,12.146L10.062,11.439Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M8.109,12C8.938,12 9.609,11.328 9.609,10.5C9.609,9.672 8.938,9 8.109,9C7.281,9 6.609,9.672 6.609,10.5C6.609,11.328 7.281,12 8.109,12Z" /> + </vector> diff --git a/packages/SettingsLib/res/drawable/ic_wifi_3_error.xml b/packages/SettingsLib/res/drawable/ic_wifi_3_error.xml index 940781bbb1ca..c30affad6a80 100644 --- a/packages/SettingsLib/res/drawable/ic_wifi_3_error.xml +++ b/packages/SettingsLib/res/drawable/ic_wifi_3_error.xml @@ -1,25 +1,49 @@ +<!-- + Copyright (C) 2025 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. +--> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="17dp" - android:height="13dp" - android:viewportWidth="17.0" - android:viewportHeight="13.0"> + android:width="21.0dp" + android:height="15.66dp" + android:viewportHeight="13.5" + android:viewportWidth="18.1"> + + <group> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <clip-path android:pathData=" + M0,0 + H18.1 + V13.5 + H0 + Z + M15.109,13.5C17.871,13.5 20.109,11.261 20.109,8.5C20.109,5.739 17.871,3.5 15.109,3.5C12.348,3.5 10.109,5.739 10.109,8.5C10.109,11.261 12.348,13.5 15.109,13.5Z" /> + + <path + android:fillColor="#000" + android:pathData="M8.109,12C8.938,12 9.609,11.328 9.609,10.5C9.609,9.672 8.938,9 8.109,9C7.281,9 6.609,9.672 6.609,10.5C6.609,11.328 7.281,12 8.109,12Z" /> + + <path + android:fillColor="#000" + android:pathData="M12.659,7.45C13.199,6.91 13.149,6.01 12.479,5.64C11.179,4.92 9.689,4.51 8.109,4.51C6.529,4.51 5.029,4.92 3.739,5.64C3.069,6.01 3.019,6.91 3.559,7.45C3.999,7.89 4.699,7.94 5.259,7.66C6.119,7.24 7.089,7 8.109,7C9.129,7 10.099,7.24 10.959,7.66C11.519,7.94 12.219,7.89 12.659,7.45Z" /> + + <path + android:fillColor="#000" + android:pathData="M15.85,4.26C16.36,3.75 16.34,2.91 15.76,2.49C13.61,0.93 10.97,0 8.11,0C5.25,0 2.61,0.92 0.46,2.49C-0.12,2.91 -0.14,3.75 0.37,4.26C0.84,4.73 1.59,4.75 2.13,4.37C3.83,3.19 5.89,2.5 8.11,2.5C10.33,2.5 12.39,3.19 14.09,4.37C14.63,4.75 15.38,4.73 15.85,4.26Z" /> + </group> + <path - android:pathData="M0.146,4.015C-0.05,3.819 -0.05,3.502 0.153,3.314C4.002,-0.244 9.545,-0.937 14.055,1.234C13.339,1.449 12.792,2.053 12.66,2.801C8.998,1.281 4.65,1.924 1.568,4.73C1.364,4.916 1.048,4.917 0.853,4.722L0.146,4.015Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M12.63,4.435C9.406,2.836 5.424,3.288 2.63,5.792C2.424,5.976 2.425,6.294 2.621,6.49L3.328,7.197C3.523,7.392 3.839,7.39 4.047,7.209C6.484,5.094 10.033,4.942 12.63,6.753V4.435Z" - android:fillColor="#000"/> - <path - android:pathData="M5.095,8.964C4.9,8.769 4.899,8.45 5.111,8.273C7.145,6.576 10.117,6.576 12.151,8.273C12.363,8.45 12.362,8.769 12.166,8.964L11.459,9.672C11.264,9.867 10.949,9.863 10.728,9.698C9.489,8.767 7.773,8.767 6.533,9.698C6.313,9.863 5.998,9.867 5.802,9.672L5.095,8.964Z" - android:fillColor="#000"/> - <path - android:pathData="M9.652,10.779C9.889,10.92 9.887,11.244 9.692,11.439L8.984,12.146C8.789,12.342 8.473,12.342 8.277,12.146L7.57,11.439C7.375,11.244 7.373,10.92 7.61,10.779C8.237,10.407 9.024,10.407 9.652,10.779Z" - android:fillColor="#000"/> - <path - android:pathData="M14.63,11.15C14.63,10.598 15.078,10.15 15.63,10.15C16.182,10.15 16.63,10.598 16.63,11.15C16.63,11.703 16.182,12.15 15.63,12.15C15.078,12.15 14.63,11.703 14.63,11.15Z" - android:fillColor="#000"/> - <path - android:pathData="M14.63,3.15C14.63,2.874 14.854,2.65 15.13,2.65H16.13C16.406,2.65 16.63,2.874 16.63,3.15V8.15C16.63,8.427 16.406,8.65 16.13,8.65H15.13C14.854,8.65 14.63,8.427 14.63,8.15V3.15Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M15.109,5C14.699,5 14.359,5.34 14.359,5.75L14.359,8.75C14.359,9.16 14.699,9.5 15.109,9.5C15.519,9.5 15.859,9.16 15.859,8.75L15.859,5.75C15.859,5.34 15.519,5 15.109,5ZM15.109,12C15.519,12 15.859,11.66 15.859,11.25C15.859,10.84 15.519,10.5 15.109,10.5C14.699,10.5 14.359,10.84 14.359,11.25C14.359,11.66 14.699,12 15.109,12Z" /> + </vector> diff --git a/packages/SettingsLib/res/drawable/ic_wifi_4.xml b/packages/SettingsLib/res/drawable/ic_wifi_4.xml deleted file mode 100644 index 6185e4a83332..000000000000 --- a/packages/SettingsLib/res/drawable/ic_wifi_4.xml +++ /dev/null @@ -1,33 +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. ---> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="18dp" - android:height="13dp" - android:viewportWidth="18.0" - android:viewportHeight="13.0"> - <path - android:pathData="M0.523,3.314C0.32,3.502 0.32,3.819 0.516,4.015L1.223,4.722C1.418,4.917 1.734,4.916 1.938,4.73C5.936,1.09 12.066,1.09 16.064,4.73C16.268,4.916 16.584,4.917 16.779,4.722L17.486,4.015C17.682,3.819 17.682,3.502 17.479,3.314C12.698,-1.105 5.304,-1.105 0.523,3.314Z" - android:fillColor="#000"/> - <path - android:pathData="M15.011,6.49C15.207,6.294 15.207,5.976 15.002,5.792C11.592,2.736 6.411,2.736 3,5.792C2.795,5.976 2.795,6.294 2.991,6.49L3.698,7.197C3.893,7.392 4.209,7.39 4.417,7.209C7.042,4.93 10.96,4.93 13.585,7.209C13.793,7.39 14.109,7.392 14.304,7.197L15.011,6.49Z" - android:fillColor="#000"/> - <path - android:pathData="M5.465,8.964C5.27,8.769 5.269,8.45 5.481,8.273C7.515,6.576 10.487,6.576 12.521,8.273C12.733,8.45 12.732,8.769 12.537,8.964L11.83,9.672C11.634,9.867 11.319,9.863 11.099,9.698C9.859,8.767 8.143,8.767 6.904,9.698C6.683,9.863 6.368,9.867 6.173,9.672L5.465,8.964Z" - android:fillColor="#000"/> - <path - android:pathData="M10.062,11.439C10.257,11.244 10.259,10.92 10.022,10.779C9.395,10.407 8.608,10.407 7.98,10.779C7.743,10.92 7.745,11.244 7.94,11.439L8.647,12.146C8.843,12.342 9.159,12.342 9.355,12.146L10.062,11.439Z" - android:fillColor="#000"/> -</vector> diff --git a/packages/SettingsLib/res/drawable/ic_wifi_4_error.xml b/packages/SettingsLib/res/drawable/ic_wifi_4_error.xml deleted file mode 100644 index 715aaa0982e9..000000000000 --- a/packages/SettingsLib/res/drawable/ic_wifi_4_error.xml +++ /dev/null @@ -1,24 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="17dp" - android:height="13dp" - android:viewportWidth="17.0" - android:viewportHeight="13.0"> - <path - android:pathData="M0.146,4.015C-0.05,3.819 -0.05,3.502 0.153,3.314C4.002,-0.244 9.545,-0.937 14.055,1.234C13.339,1.449 12.792,2.053 12.66,2.801C8.998,1.281 4.65,1.924 1.568,4.73C1.364,4.916 1.048,4.917 0.853,4.722L0.146,4.015Z" - android:fillColor="#000"/> - <path - android:pathData="M12.63,4.435C9.406,2.836 5.424,3.288 2.63,5.792C2.424,5.976 2.425,6.294 2.621,6.49L3.328,7.197C3.523,7.392 3.839,7.39 4.047,7.209C6.484,5.094 10.033,4.942 12.63,6.753V4.435Z" - android:fillColor="#000"/> - <path - android:pathData="M5.095,8.964C4.9,8.769 4.899,8.45 5.111,8.273C7.145,6.576 10.117,6.576 12.151,8.273C12.363,8.45 12.362,8.769 12.166,8.964L11.459,9.672C11.264,9.867 10.949,9.863 10.728,9.698C9.489,8.767 7.773,8.767 6.533,9.698C6.313,9.863 5.998,9.867 5.802,9.672L5.095,8.964Z" - android:fillColor="#000"/> - <path - android:pathData="M9.652,10.779C9.889,10.92 9.887,11.244 9.692,11.439L8.984,12.146C8.789,12.342 8.473,12.342 8.277,12.146L7.57,11.439C7.375,11.244 7.373,10.92 7.61,10.779C8.237,10.407 9.024,10.407 9.652,10.779Z" - android:fillColor="#000"/> - <path - android:pathData="M14.63,11.15C14.63,10.598 15.078,10.15 15.63,10.15C16.182,10.15 16.63,10.598 16.63,11.15C16.63,11.703 16.182,12.15 15.63,12.15C15.078,12.15 14.63,11.703 14.63,11.15Z" - android:fillColor="#000"/> - <path - android:pathData="M14.63,3.15C14.63,2.874 14.854,2.65 15.13,2.65H16.13C16.406,2.65 16.63,2.874 16.63,3.15V8.15C16.63,8.427 16.406,8.65 16.13,8.65H15.13C14.854,8.65 14.63,8.427 14.63,8.15V3.15Z" - android:fillColor="#000"/> -</vector> diff --git a/core/res/res/values-w204dp-round-watch/dimens_watch.xml b/packages/SettingsLib/res/layout/activity_create_new_user.xml index 3509474c5c2e..7453b53a6956 100644 --- a/core/res/res/values-w204dp-round-watch/dimens_watch.xml +++ b/packages/SettingsLib/res/layout/activity_create_new_user.xml @@ -1,5 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- +<?xml version="1.0" encoding="utf-8"?><!-- ~ Copyright (C) 2025 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +13,10 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<resources> - <!-- watch's indeterminate progress bar dimens based on the current screen size --> - <dimen name="loader_horizontal_min_width_watch">70dp</dimen> - <dimen name="loader_horizontal_min_height_watch">15dp</dimen> -</resources> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/transparent" + android:orientation="vertical"> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/res/values/styles.xml b/packages/SettingsLib/res/values/styles.xml index 3326b6034237..7ab096fae0db 100644 --- a/packages/SettingsLib/res/values/styles.xml +++ b/packages/SettingsLib/res/values/styles.xml @@ -116,4 +116,13 @@ <item name="android:textAppearance">?android:attr/textAppearanceSmall</item> <item name="android:textSize">16dp</item> </style> + + <style name="Theme.Transparent" parent="@android:style/Theme.DeviceDefault.Settings"> + <item name="android:windowActionBar">false</item> + <item name="android:windowNoTitle">true</item> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowContentOverlay">@null</item> + </style> + </resources> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index 572444edea29..bf86911ee683 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -30,13 +30,11 @@ import android.util.Log; import androidx.annotation.ChecksSdkIntAtLeast; import com.android.internal.annotations.VisibleForTesting; -import com.android.settingslib.flags.Flags; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -387,7 +385,7 @@ public class CsipDeviceManager { preferredMainDevice.refresh(); hasChanged = true; } - syncAudioSharingStatusIfNeeded(preferredMainDevice); + syncAudioSharingSourceIfNeeded(preferredMainDevice); } if (hasChanged) { log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " @@ -401,16 +399,13 @@ public class CsipDeviceManager { return userManager != null && userManager.isManagedProfile(); } - private void syncAudioSharingStatusIfNeeded(CachedBluetoothDevice mainDevice) { + private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) { boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingUIAvailable(mContext); - if (isAudioSharingEnabled && mainDevice != null) { + if (isAudioSharingEnabled) { if (isWorkProfile()) { - log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, work profile"); + log("addMemberDevicesIntoMainDevice: skip sync source for work profile"); return; } - Set<CachedBluetoothDevice> deviceSet = new HashSet<>(); - deviceSet.add(mainDevice); - deviceSet.addAll(mainDevice.getMemberDevice()); boolean hasBroadcastSource = BluetoothUtils.isBroadcasting(mBtManager) && BluetoothUtils.hasConnectedBroadcastSource( mainDevice, mBtManager); @@ -424,6 +419,9 @@ public class CsipDeviceManager { if (metadata != null && assistant != null) { log("addMemberDevicesIntoMainDevice: sync audio sharing source after " + "combining the top level devices."); + Set<CachedBluetoothDevice> deviceSet = new HashSet<>(); + deviceSet.add(mainDevice); + deviceSet.addAll(mainDevice.getMemberDevice()); Set<BluetoothDevice> sinksToSync = deviceSet.stream() .map(CachedBluetoothDevice::getDevice) .filter(device -> @@ -437,24 +435,8 @@ public class CsipDeviceManager { } } } - if (Flags.enableTemporaryBondDevicesUi()) { - log("addMemberDevicesIntoMainDevice: sync temp bond metadata for audio sharing " - + "sinks after combining the top level devices."); - Set<BluetoothDevice> sinksToSync = deviceSet.stream() - .map(CachedBluetoothDevice::getDevice).filter(Objects::nonNull).collect( - Collectors.toSet()); - if (sinksToSync.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice)) { - for (BluetoothDevice device : sinksToSync) { - if (!BluetoothUtils.isTemporaryBondDevice(device)) { - log("addMemberDevicesIntoMainDevice: sync temp bond metadata for " - + device.getAnonymizedAddress()); - BluetoothUtils.setTemporaryBondMetadata(device); - } - } - } - } } else { - log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, flag disabled"); + log("addMemberDevicesIntoMainDevice: skip sync source, flag disabled"); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 84156429809b..367e38ed779d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -63,13 +63,14 @@ import com.google.common.collect.ImmutableList; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -84,6 +85,8 @@ import java.util.stream.Collectors; public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { public static final String ACTION_LE_AUDIO_SHARING_STATE_CHANGE = "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE"; + public static final String ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED = + "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_DEVICE_CONNECTED"; public static final String EXTRA_LE_AUDIO_SHARING_STATE = "BLUETOOTH_LE_AUDIO_SHARING_STATE"; public static final String EXTRA_BLUETOOTH_DEVICE = "BLUETOOTH_DEVICE"; public static final String EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE = "BT_DEVICE_TO_AUTO_ADD_SOURCE"; @@ -105,6 +108,10 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private static final String SYSUI_PKG = "com.android.systemui"; private static final String TAG = "LocalBluetoothLeBroadcast"; private static final boolean DEBUG = BluetoothUtils.D; + private static final String VALID_PASSWORD_CHARACTERS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:," + + ".<>?/"; + private static final int PASSWORD_LENGTH = 16; static final String NAME = "LE_AUDIO_BROADCAST"; private static final String UNDERLINE = "_"; @@ -1085,11 +1092,16 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { mBroadcastId = UNKNOWN_VALUE_PLACEHOLDER; } - private String generateRandomPassword() { - String randomUUID = UUID.randomUUID().toString(); - // first 16 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - return randomUUID.substring(0, 8) + randomUUID.substring(9, 13) + randomUUID.substring(14, - 18); + private static String generateRandomPassword() { + SecureRandom random = new SecureRandom(); + StringBuilder stringBuilder = new StringBuilder(PASSWORD_LENGTH); + + for (int i = 0; i < PASSWORD_LENGTH; i++) { + int randomIndex = random.nextInt(VALID_PASSWORD_CHARACTERS.length()); + stringBuilder.append(VALID_PASSWORD_CHARACTERS.charAt(randomIndex)); + } + + return stringBuilder.toString(); } private void registerContentObserver() { @@ -1189,6 +1201,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { @NonNull private Map<Integer, List<BluetoothDevice>> getDeviceGroupsInBroadcast() { + if (mServiceBroadcastAssistant == null) return new HashMap<>(); boolean hysteresisModeFixEnabled = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext); List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices(); 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 d5cfe55813ee..460c790174ab 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java @@ -35,6 +35,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.SuppressLint; import android.app.AutomaticZenRule; import android.app.NotificationManager; +import android.content.ComponentName; import android.content.Context; import android.net.Uri; import android.os.Parcel; @@ -117,6 +118,14 @@ public class ZenMode implements Parcelable { DISABLED_BY_OTHER } + /** + * Information about the owner of a {@link ZenMode}. {@link #packageName()} is + * {@link SystemZenRules#PACKAGE_ANDROID} if the mode is system-owned; it may also be + * {@code null}, but only as an artifact of very old modes. + */ + public record Owner(@Nullable String packageName, @Nullable ComponentName configurationActivity, + @Nullable ComponentName conditionProvider) { } + private final String mId; private final AutomaticZenRule mRule; private final Kind mKind; @@ -198,7 +207,7 @@ public class ZenMode implements Parcelable { } @NonNull - public AutomaticZenRule getRule() { + AutomaticZenRule getRule() { return mRule; } @@ -207,6 +216,10 @@ public class ZenMode implements Parcelable { return Strings.nullToEmpty(mRule.getName()); } + public void setName(@NonNull String name) { + mRule.setName(name); + } + @NonNull public Kind getKind() { return mKind; @@ -217,6 +230,17 @@ public class ZenMode implements Parcelable { return mStatus; } + @NonNull + public Owner getOwner() { + return new Owner(mRule.getPackageName(), mRule.getConfigurationActivity(), + mRule.getOwner()); + } + + @Nullable + public String getOwnerPackage() { + return getOwner().packageName(); + } + @AutomaticZenRule.Type public int getType() { return mRule.getType(); @@ -257,6 +281,26 @@ public class ZenMode implements Parcelable { } } + /** + * Returns the resource id of the icon for this mode. Note that this is the <em>stored</em> + * resource id, and thus can be different from the value in {@link #getIconKey()} -- in + * particular, for modes without a custom icon set, this method returns {@code 0} whereas + * {@link #getIconKey()} will return a default icon based on other mode properties. + * + * <p>Most callers are interested in {@link #getIconKey()}, unless they are editing the icon. + */ + public int getIconResId() { + return mRule.getIconResId(); + } + + /** + * Sets the resource id of the icon for this mode. + * @see #getIconResId() + */ + public void setIconResId(@DrawableRes int iconResId) { + mRule.setIconResId(iconResId); + } + /** Returns the interruption filter of the mode. */ @NotificationManager.InterruptionFilter public int getInterruptionFilter() { @@ -445,6 +489,10 @@ public class ZenMode implements Parcelable { return mStatus == Status.ENABLED_AND_ACTIVE; } + public boolean isManualInvocationAllowed() { + return mRule.isManualInvocationAllowed(); + } + public boolean isSystemOwned() { return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()); } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeDescriptions.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeDescriptions.java index f5776989917e..b67339bace67 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeDescriptions.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeDescriptions.java @@ -70,8 +70,7 @@ public final class ZenModeDescriptions { public String getTriggerDescriptionForAccessibility(@NonNull ZenMode mode) { // Only one special case: time-based schedules, where we want to use full day names. if (mode.isSystemOwned() && mode.getType() == TYPE_SCHEDULE_TIME) { - ZenModeConfig.ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId( - mode.getRule().getConditionId()); + ZenModeConfig.ScheduleInfo schedule = ZenModeSchedules.getTimeSchedule(mode); if (schedule != null) { String fullDaysSummary = SystemZenRules.getDaysOfWeekFull(mContext, schedule); if (fullDaysSummary != null) { diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeSchedules.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeSchedules.java new file mode 100644 index 000000000000..c981652f231d --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeSchedules.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.notification.modes; + +import android.service.notification.ZenModeConfig; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ZenModeSchedules { + + /** + * Returns the {@link ZenModeConfig.ScheduleInfo} time schedule corresponding to the mode, or + * {@code null} if the mode is not time-schedule-based. + */ + @Nullable + public static ZenModeConfig.ScheduleInfo getTimeSchedule(@NonNull ZenMode mode) { + return ZenModeConfig.tryParseScheduleConditionId(mode.getRule().getConditionId()); + } + + /** + * Returns the {@link ZenModeConfig.EventInfo} calendar schedule corresponding to the mode, or + * {@code null} if the mode is not calendar-schedule-based. + */ + @Nullable + public static ZenModeConfig.EventInfo getCalendarSchedule(@NonNull ZenMode mode) { + return ZenModeConfig.tryParseEventConditionId(mode.getRule().getConditionId()); + } + + private ZenModeSchedules() { } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java new file mode 100644 index 000000000000..c5e6f60e3fa6 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.users; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.R; + + +public class CreateUserActivity extends Activity { + private static final String TAG = "CreateUserActivity"; + + public static final String EXTRA_USER_NAME = "new_user_name"; + public static final String EXTRA_IS_ADMIN = "is_admin"; + public static final String EXTRA_USER_ICON_PATH = "user_icon_path"; + private static final String DIALOG_STATE_KEY = "create_user_dialog_state"; + private static final String EXTRA_CAN_CREATE_ADMIN = "can_create_admin"; + private static final String EXTRA_FILE_AUTHORITY = "file_authority"; + + private CreateUserDialogController mCreateUserDialogController; + @VisibleForTesting + Dialog mSetupUserDialog; + + + /** + * Creates intent to start CreateUserActivity + */ + public static @NonNull Intent createIntentForStart(@NonNull Context context, + boolean canCreateAdminUser, @NonNull String fileAuth) { + Intent intent = new Intent(context, CreateUserActivity.class); + intent.putExtra(EXTRA_CAN_CREATE_ADMIN, canCreateAdminUser); + intent.putExtra(EXTRA_FILE_AUTHORITY, fileAuth); + return intent; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + + mCreateUserDialogController = new CreateUserDialogController( + intent.getStringExtra(EXTRA_FILE_AUTHORITY)); + setContentView(R.layout.activity_create_new_user); + if (savedInstanceState != null) { + mCreateUserDialogController.onRestoreInstanceState(savedInstanceState); + } + mSetupUserDialog = createDialog(intent.getBooleanExtra(EXTRA_CAN_CREATE_ADMIN, false)); + mSetupUserDialog.show(); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + Bundle savedDialogState = savedInstanceState.getBundle(DIALOG_STATE_KEY); + if (savedDialogState != null && mSetupUserDialog != null) { + mSetupUserDialog.onRestoreInstanceState(savedDialogState); + } + } + + private Dialog createDialog(boolean canCreateAdminUser) { + return mCreateUserDialogController.createDialog( + this, + this::startActivity, + canCreateAdminUser, + this::setSuccessResult, + this::cancel + ); + } + + @Override + public boolean onTouchEvent(@Nullable MotionEvent event) { + onBackInvoked(); + return super.onTouchEvent(event); + } + + private void onBackInvoked() { + if (mSetupUserDialog != null) { + mSetupUserDialog.dismiss(); + } + setResult(RESULT_CANCELED); + finish(); + } + + @VisibleForTesting + void setSuccessResult(String userName, Drawable userIcon, String path, Boolean isAdmin) { + Intent intent = new Intent(this, CreateUserActivity.class); + intent.putExtra(EXTRA_USER_NAME, userName); + intent.putExtra(EXTRA_IS_ADMIN, isAdmin); + intent.putExtra(EXTRA_USER_ICON_PATH, path); + + mSetupUserDialog.dismiss(); + setResult(RESULT_OK, intent); + finish(); + } + + @VisibleForTesting + void cancel() { + mSetupUserDialog.dismiss(); + setResult(RESULT_CANCELED); + finish(); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + if (mSetupUserDialog != null && mSetupUserDialog.isShowing()) { + outState.putBundle(DIALOG_STATE_KEY, mSetupUserDialog.onSaveInstanceState()); + } + mCreateUserDialogController.onSaveInstanceState(outState); + super.onSaveInstanceState(outState); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mCreateUserDialogController.onActivityResult(requestCode, resultCode, data); + } + + private void startActivity(Intent intent, int requestCode) { + startActivityForResult(intent, requestCode); + mCreateUserDialogController.startingActivityForResult(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java index d71b337228f6..d9f1b632323c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java @@ -242,7 +242,7 @@ public class CreateUserDialogController { .setMessage(messageResId) .setNegativeButtonText(R.string.cancel) .setPositiveButtonText(R.string.next); - mCustomDialogHelper.requestFocusOnTitle(); + focus(); break; case GRANT_ADMIN_DIALOG: mEditUserInfoView.setVisibility(View.GONE); @@ -255,7 +255,7 @@ public class CreateUserDialogController { .setMessage(R.string.user_grant_admin_message) .setNegativeButtonText(R.string.back) .setPositiveButtonText(R.string.next); - mCustomDialogHelper.requestFocusOnTitle(); + focus(); if (mIsAdmin == null) { mCustomDialogHelper.setButtonEnabled(false); } @@ -267,7 +267,7 @@ public class CreateUserDialogController { .setTitle(R.string.user_info_settings_title) .setNegativeButtonText(R.string.back) .setPositiveButtonText(R.string.done); - mCustomDialogHelper.requestFocusOnTitle(); + focus(); mEditUserInfoView.setVisibility(View.VISIBLE); mGrantAdminView.setVisibility(View.GONE); break; @@ -282,7 +282,7 @@ public class CreateUserDialogController { mCustomDialogHelper.getDialog().dismiss(); break; case EXIT_DIALOG: - mCustomDialogHelper.getDialog().dismiss(); + finish(); break; default: if (mCurrentState < EXIT_DIALOG) { @@ -394,13 +394,21 @@ public class CreateUserDialogController { return mCustomDialogHelper != null && mCustomDialogHelper.getDialog() != null; } + void focus() { + mCustomDialogHelper.requestFocusOnTitle(); + } + /** * Runs callback and clears saved values after dialog is dismissed. */ public void finish() { if (mCurrentState == CREATE_USER_AND_CLOSE) { if (mSuccessCallback != null) { - mSuccessCallback.onSuccess(mUserName, mNewUserIcon, Boolean.TRUE.equals(mIsAdmin)); + if (mEditUserPhotoController != null && mCachedDrawablePath == null) { + mCachedDrawablePath = mEditUserPhotoController.getCachedDrawablePath(); + } + mSuccessCallback.onSuccess(mUserName, mNewUserIcon, mCachedDrawablePath, + Boolean.TRUE.equals(mIsAdmin)); } } else { if (mCancelCallback != null) { diff --git a/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java b/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java index 3d18b59258b3..eed608e98cc9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java @@ -18,6 +18,8 @@ package com.android.settingslib.users; import android.graphics.drawable.Drawable; +import androidx.annotation.Nullable; + /** * Defines a callback when a new user data is filled out. */ @@ -27,8 +29,10 @@ public interface NewUserData { * Consumes data relevant to new user that needs to be created. * @param userName New user name. * @param userImage New user icon. + * @param iconPath New user icon path. * @param isNewUserAdmin A boolean that indicated whether new user has admin status. */ - void onSuccess(String userName, Drawable userImage, Boolean isNewUserAdmin); + void onSuccess(@Nullable String userName, @Nullable Drawable userImage, + @Nullable String iconPath, @Nullable Boolean isNewUserAdmin); } diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt index c71b19c9235f..88bccc9f6ebd 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt @@ -119,12 +119,16 @@ open class WifiUtils { private fun getIconsBasedOnFlag(): IntArray { return if (newStatusBarIcons()) { + // TODO(b/396664075): + // The new wifi icons only define a range of [0, 3]. Since this array is indexed on + // level, we can simulate the range squash by mapping both level 3 to drawn-level 2, + // and level 4 to drawn-level 3 intArrayOf( R.drawable.ic_wifi_0, R.drawable.ic_wifi_1, R.drawable.ic_wifi_2, + R.drawable.ic_wifi_2, R.drawable.ic_wifi_3, - R.drawable.ic_wifi_4 ) } else { intArrayOf( @@ -141,12 +145,13 @@ open class WifiUtils { private fun getErrorIconsBasedOnFlag(): IntArray { return if (newStatusBarIcons()) { + // See above note, new wifi icons only have 3 bars, so levels 2 and 3 are the same intArrayOf( R.drawable.ic_wifi_0_error, R.drawable.ic_wifi_1_error, R.drawable.ic_wifi_2_error, + R.drawable.ic_wifi_2_error, R.drawable.ic_wifi_3_error, - R.drawable.ic_wifi_4_error ) } else { intArrayOf( diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java index 2eccaa626f3b..fd14d1ff6786 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -40,8 +40,6 @@ import android.content.Context; import android.os.Looper; import android.os.Parcel; import android.os.UserManager; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import com.android.settingslib.flags.Flags; @@ -76,9 +74,6 @@ public class CsipDeviceManagerTest { private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; private final static String DEVICE_ADDRESS_3 = "AA:BB:CC:DD:EE:33"; - private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; - private static final String TEMP_BOND_METADATA = - "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>"; private final static int GROUP1 = 1; private final BluetoothClass DEVICE_CLASS_1 = createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES); @@ -342,7 +337,6 @@ public class CsipDeviceManagerTest { } @Test - @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_returnTrue() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list @@ -352,6 +346,7 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -364,7 +359,6 @@ public class CsipDeviceManagerTest { } @Test - @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_workProfile_doNothing() { // Condition: The preferredDevice is main and there is another main device in top list @@ -375,6 +369,7 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -382,8 +377,6 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); - when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) - .thenReturn(TEMP_BOND_METADATA.getBytes()); when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); when(mUserManager.isManagedProfile()).thenReturn(true); @@ -394,13 +387,10 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); verify(mAssistant, never()).addSource(mDevice1, metadata, /* isGroupOp= */ false); - verify(mDevice1, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, - TEMP_BOND_METADATA.getBytes()); } @Test - @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) - public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncState() { + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncSource() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice1; @@ -409,6 +399,7 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -416,8 +407,6 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); - when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) - .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -426,8 +415,6 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); verify(mAssistant).addSource(mDevice1, metadata, /* isGroupOp= */ false); - verify(mDevice1).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, - TEMP_BOND_METADATA.getBytes()); } @Test @@ -449,13 +436,13 @@ public class CsipDeviceManagerTest { } @Test - @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_returnTrue() { // Condition: The preferredDevice is member and there are two main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice2; BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); mCachedDevice3.setGroupId(GROUP1); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(false); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) @@ -470,20 +457,16 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); verify(mAssistant, never()).addSource(any(BluetoothDevice.class), any(BluetoothLeBroadcastMetadata.class), anyBoolean()); - verify(mDevice2, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, - TEMP_BOND_METADATA.getBytes()); - verify(mDevice3, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, - TEMP_BOND_METADATA.getBytes()); } @Test - @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) - public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncState() { + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncSource() { // Condition: The preferredDevice is member and there are two main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice2; BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); mCachedDevice3.setGroupId(GROUP1); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -491,8 +474,6 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of(state)); - when(mDevice1.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) - .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -507,10 +488,6 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); verify(mAssistant).addSource(mDevice2, metadata, /* isGroupOp= */ false); verify(mAssistant).addSource(mDevice3, metadata, /* isGroupOp= */ false); - verify(mDevice2).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, - TEMP_BOND_METADATA.getBytes()); - verify(mDevice3).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, - TEMP_BOND_METADATA.getBytes()); } @Test 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 6b30f159129e..71ecf5b76296 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 @@ -21,6 +21,7 @@ import static android.app.AutomaticZenRule.TYPE_DRIVING; import static android.app.AutomaticZenRule.TYPE_IMMERSIVE; import static android.app.AutomaticZenRule.TYPE_OTHER; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; +import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; import static android.app.AutomaticZenRule.TYPE_THEATER; import static android.app.AutomaticZenRule.TYPE_UNKNOWN; import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS; @@ -35,6 +36,8 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import android.app.AutomaticZenRule; +import android.content.ComponentName; +import android.content.Context; import android.net.Uri; import android.os.Parcel; import android.service.notification.Condition; @@ -43,13 +46,17 @@ import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; +import androidx.test.core.app.ApplicationProvider; + import com.android.internal.R; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import java.util.ArrayList; +import java.util.Calendar; import java.util.List; @RunWith(RobolectricTestRunner.class) @@ -72,6 +79,13 @@ public class ZenModeTest { .setType(TYPE_OTHER) .build(); + private Context mContext; + + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + } + @Test public void testBasicMethods_mode() { ZenMode zenMode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, true)); @@ -95,7 +109,7 @@ public class ZenModeTest { assertThat(manualMode.canEditPolicy()).isTrue(); assertThat(manualMode.canBeDeleted()).isFalse(); assertThat(manualMode.isActive()).isFalse(); - assertThat(manualMode.getRule().getPackageName()).isEqualTo(PACKAGE_ANDROID); + assertThat(manualMode.getOwnerPackage()).isEqualTo(PACKAGE_ANDROID); } @Test @@ -153,7 +167,7 @@ public class ZenModeTest { public void isCustomManual_scheduleTime_false() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Mode", Uri.parse("x")) .setPackage(SystemZenRules.PACKAGE_ANDROID) - .setType(AutomaticZenRule.TYPE_SCHEDULE_TIME) + .setType(TYPE_SCHEDULE_TIME) .build(); ZenMode mode = new ZenMode("id", rule, zenConfigRuleFor(rule, false)); @@ -194,6 +208,23 @@ public class ZenModeTest { } @Test + public void getOwner_returnsOwnerDetails() { + AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) + .setPackage("package") + .setConfigurationActivity(new ComponentName("package", "configActivity")) + .setOwner(new ComponentName("package", "conditionService")) + .build(); + ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false)); + + ZenMode.Owner owner = zenMode.getOwner(); + assertThat(owner.packageName()).isEqualTo("package"); + assertThat(owner.configurationActivity()).isEqualTo( + new ComponentName("package", "configActivity")); + assertThat(owner.conditionProvider()).isEqualTo( + new ComponentName("package", "conditionService")); + } + + @Test public void getPolicy_interruptionFilterPriority_returnsZenPolicy() { AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) @@ -245,7 +276,7 @@ public class ZenModeTest { zenMode.setPolicy(ZEN_POLICY); - assertThat(zenMode.getRule().getInterruptionFilter()).isEqualTo( + assertThat(zenMode.getInterruptionFilter()).isEqualTo( INTERRUPTION_FILTER_PRIORITY); assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY); assertThat(zenMode.getRule().getZenPolicy()).isEqualTo(ZEN_POLICY); @@ -397,6 +428,7 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isEqualTo("some.package"); assertThat(iconKey.resId()).isEqualTo(123); + assertThat(mode.getIconResId()).isEqualTo(123); } @Test @@ -411,6 +443,7 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo(123); + assertThat(mode.getIconResId()).isEqualTo(123); } @Test @@ -425,15 +458,18 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isEqualTo("some.package"); assertThat(iconKey.resId()).isEqualTo(123); + assertThat(mode.getIconResId()).isEqualTo(123); } @Test public void getIconKey_manualDnd_isDndIcon() { - ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND.getIconKey(); + ZenMode mode = TestModeBuilder.MANUAL_DND; + ZenIcon.Key iconKey = mode.getIconKey(); assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( com.android.internal.R.drawable.ic_zen_mode_type_special_dnd); + assertThat(mode.getIconResId()).isEqualTo(0); } @Test @@ -448,6 +484,7 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( com.android.internal.R.drawable.ic_zen_mode_type_bedtime); + assertThat(mode.getIconResId()).isEqualTo(0); } @Test @@ -462,6 +499,7 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar); + assertThat(mode.getIconResId()).isEqualTo(0); } @Test @@ -477,6 +515,77 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( com.android.internal.R.drawable.ic_zen_mode_type_special_dnd); + assertThat(mode.getIconResId()).isEqualTo(0); + } + + @Test + public void setCustomModeConditionId_timeSchedule() { + ZenMode mode = new TestModeBuilder() + .setPackage(PACKAGE_ANDROID) + .build(); + ZenModeConfig.ScheduleInfo timeSchedule = new ZenModeConfig.ScheduleInfo(); + timeSchedule.startHour = 9; + timeSchedule.endHour = 12; + timeSchedule.days = new int[] {Calendar.SATURDAY, Calendar.SUNDAY}; + Uri scheduleUri = ZenModeConfig.toScheduleConditionId(timeSchedule); + + mode.setCustomModeConditionId(mContext, scheduleUri); + + assertThat(mode.getType()).isEqualTo(TYPE_SCHEDULE_TIME); + assertThat(ZenModeSchedules.getTimeSchedule(mode)).isEqualTo(timeSchedule); + assertThat(mode.getTriggerDescription()).isEqualTo("Sun, Sat, 9:00 AM - 12:00 PM"); + + assertThat(mode.getRule().getConditionId()).isEqualTo(scheduleUri); + assertThat(mode.getRule().getOwner()).isEqualTo( + ZenModeConfig.getScheduleConditionProvider()); + } + + @Test + public void setCustomModeConditionId_calendarSchedule() { + ZenMode mode = new TestModeBuilder() + .setPackage(PACKAGE_ANDROID) + .build(); + ZenModeConfig.EventInfo calendarSchedule = new ZenModeConfig.EventInfo(); + calendarSchedule.calendarId = 1L; + calendarSchedule.calName = "My events"; + Uri scheduleUri = ZenModeConfig.toEventConditionId(calendarSchedule); + + mode.setCustomModeConditionId(mContext, scheduleUri); + + assertThat(mode.getType()).isEqualTo(TYPE_SCHEDULE_CALENDAR); + assertThat(ZenModeSchedules.getCalendarSchedule(mode)).isEqualTo(calendarSchedule); + assertThat(mode.getTriggerDescription()).isEqualTo("My events"); + + assertThat(mode.getRule().getConditionId()).isEqualTo(scheduleUri); + assertThat(mode.getRule().getOwner()).isEqualTo( + ZenModeConfig.getEventConditionProvider()); + } + + @Test + public void setCustomModeConditionId_manualSchedule() { + ZenMode mode = new TestModeBuilder() + .setPackage(PACKAGE_ANDROID) + .build(); + + mode.setCustomModeConditionId(mContext, ZenModeConfig.toCustomManualConditionId()); + + assertThat(mode.getType()).isEqualTo(TYPE_OTHER); + assertThat(mode.getTriggerDescription()).isEqualTo(""); + + assertThat(mode.getRule().getConditionId()).isEqualTo( + ZenModeConfig.toCustomManualConditionId()); + assertThat(mode.getRule().getOwner()).isEqualTo( + ZenModeConfig.getCustomManualConditionProvider()); + } + + @Test + public void setCustomModeConditionId_nonSystemRule_throws() { + ZenMode mode = new TestModeBuilder() + .setPackage("some.other.package") + .build(); + + assertThrows(IllegalStateException.class, + () -> mode.setCustomModeConditionId(mContext, Uri.parse("blah"))); } private static void assertUnparceledIsEqualToOriginal(String type, ZenMode original) { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java new file mode 100644 index 000000000000..f58eb7cc2e31 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.users; + +import static com.android.settingslib.users.CreateUserActivity.EXTRA_IS_ADMIN; +import static com.android.settingslib.users.CreateUserActivity.EXTRA_USER_ICON_PATH; +import static com.android.settingslib.users.CreateUserActivity.EXTRA_USER_NAME; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.MotionEvent; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class CreateUserActivityTest { + + private static final String TEST_USER_NAME = "test_user"; + private static final String TEST_USER_ICON_PATH = "/test_path"; + private static final boolean TEST_IS_ADMIN = true; + + private Context mContext; + private CreateUserActivity mCreateUserActivity; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mCreateUserActivity = Robolectric.buildActivity(CreateUserActivity.class).setup().get(); + } + + @Test + public void startActivity_startsActivityForResult() { + Intent activityIntent = CreateUserActivity.createIntentForStart(mContext, true, ""); + mCreateUserActivity.startActivity(activityIntent, null); + + assertThat(shadowOf(mCreateUserActivity).getNextStartedActivityForResult().intent) + .isEqualTo(activityIntent); + } + + @Test + public void onTouchEvent_dismissesDialogAndCancelsResult() { + mCreateUserActivity.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, + 0)); + + assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse(); + assertThat(shadowOf(mCreateUserActivity).getResultCode()) + .isEqualTo(Activity.RESULT_CANCELED); + } + + @Test + public void setSuccessResult_dismissesDialogAndSetsSuccessResult() { + Drawable mockDrawable = mock(Drawable.class); + + mCreateUserActivity.setSuccessResult(TEST_USER_NAME, mockDrawable, TEST_USER_ICON_PATH, + TEST_IS_ADMIN); + + assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse(); + assertThat(shadowOf(mCreateUserActivity).getResultCode()).isEqualTo(Activity.RESULT_OK); + + Intent resultIntent = shadowOf(mCreateUserActivity).getResultIntent(); + assertThat(resultIntent.getStringExtra(EXTRA_USER_NAME)).isEqualTo(TEST_USER_NAME); + assertThat(resultIntent.getBooleanExtra(EXTRA_IS_ADMIN, false)).isEqualTo(TEST_IS_ADMIN); + assertThat(resultIntent.getStringExtra(EXTRA_USER_ICON_PATH)) + .isEqualTo(TEST_USER_ICON_PATH); + } + + @Test + public void cancel_dismissesDialogAndSetsCancelResult() { + mCreateUserActivity.cancel(); + + assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse(); + assertThat(shadowOf(mCreateUserActivity).getResultCode()) + .isEqualTo(Activity.RESULT_CANCELED); + } + + @Test + public void onSaveInstanceState_savesDialogState() { + Bundle outState = new Bundle(); + mCreateUserActivity.onSaveInstanceState(outState); + + CreateUserActivity restoredActivity = + Robolectric.buildActivity(CreateUserActivity.class).setup(outState).get(); + + assertThat(restoredActivity.mSetupUserDialog).isNotNull(); + assertThat(restoredActivity.mSetupUserDialog.isShowing()).isTrue(); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java index 68312223b4b1..e60232339e4c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java @@ -211,7 +211,7 @@ public class CreateUserDialogControllerTest { editText.setText(expectedNewName); next.performClick(); verify(successCallback, times(1)) - .onSuccess(expectedNewName, null, true); + .onSuccess(expectedNewName, null, null, true); verifyNoInteractions(cancelCallback); } @@ -233,7 +233,7 @@ public class CreateUserDialogControllerTest { editText.setText(expectedNewName); next.performClick(); verify(successCallback, times(1)) - .onSuccess(expectedNewName, null, false); + .onSuccess(expectedNewName, null, null, false); verifyNoInteractions(cancelCallback); } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 8e3aa65fa5c7..bc281eea39d8 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -1503,7 +1503,7 @@ public class SettingsProvider extends ContentProvider { if (DEBUG) { Slog.v(LOG_TAG, "insertGlobalSetting(" + name + ", " + value + ", " + ", " + tag + ", " + makeDefault + ", " + requestingUserId - + ", " + forceNotify + ")"); + + ", " + forceNotify + ", " + overrideableByRestore + ")"); } return mutateGlobalSetting(name, value, tag, makeDefault, requestingUserId, MUTATION_OPERATION_INSERT, forceNotify, 0, overrideableByRestore); @@ -1785,7 +1785,7 @@ public class SettingsProvider extends ContentProvider { if (DEBUG) { Slog.v(LOG_TAG, "insertSecureSetting(" + name + ", " + value + ", " + ", " + tag + ", " + makeDefault + ", " + requestingUserId - + ", " + forceNotify + ")"); + + ", " + forceNotify + ", " + overrideableByRestore + ")"); } return mutateSecureSetting(name, value, tag, makeDefault, requestingUserId, MUTATION_OPERATION_INSERT, forceNotify, 0, overrideableByRestore); @@ -1946,7 +1946,7 @@ public class SettingsProvider extends ContentProvider { boolean overrideableByRestore) { if (DEBUG) { Slog.v(LOG_TAG, "insertSystemSetting(" + name + ", " + value + ", " - + requestingUserId + ")"); + + requestingUserId + ", " + overrideableByRestore + ")"); } return mutateSystemSetting(name, value, /* tag= */ null, requestingUserId, diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java index 17ebf6fc3235..0484defeb4d7 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java @@ -30,6 +30,7 @@ import android.os.ShellCommand; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.util.Slog; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -94,6 +95,8 @@ final public class SettingsService extends Binder { } final static class MyShellCommand extends ShellCommand { + private static final String LOG_TAG = "SettingsShellCmd"; + final SettingsProvider mProvider; final boolean mDumping; @@ -115,6 +118,7 @@ final public class SettingsService extends Binder { String mTag = null; int mResetMode = -1; boolean mMakeDefault; + boolean mOverrideableByRestore; MyShellCommand(SettingsProvider provider, boolean dumping) { mProvider = provider; @@ -209,6 +213,7 @@ final public class SettingsService extends Binder { return -1; } break; + // At this point, mVerb == PUT } else if (mKey == null) { mKey = arg; // keep going; there's another PUT arg @@ -217,36 +222,8 @@ final public class SettingsService extends Binder { // what we have so far is a valid command valid = true; // keep going; there may be another PUT arg - } else if (mTag == null) { - mTag = arg; - if ("default".equalsIgnoreCase(mTag)) { - mTag = null; - mMakeDefault = true; - if (peekNextArg() == null) { - valid = true; - } else { - perr.println("Too many arguments"); - return -1; - } - break; - } - if (peekNextArg() == null) { - valid = true; - break; - } - } else { // PUT, final arg - if (!"default".equalsIgnoreCase(arg)) { - perr.println("Argument expected to be 'default'"); - return -1; - } - mMakeDefault = true; - if (peekNextArg() == null) { - valid = true; - } else { - perr.println("Too many arguments"); - return -1; - } - break; + } else { + valid = parseOptionalPutArgument(arg); } } while ((arg = getNextArg()) != null); @@ -275,7 +252,8 @@ final public class SettingsService extends Binder { pout.println(getForUser(iprovider, mUser, mTable, mKey)); break; case PUT: - putForUser(iprovider, mUser, mTable, mKey, mValue, mTag, mMakeDefault); + putForUser(iprovider, mUser, mTable, mKey, mValue, mTag, mMakeDefault, + mOverrideableByRestore); break; case DELETE: pout.println("Deleted " @@ -297,6 +275,41 @@ final public class SettingsService extends Binder { return 0; } + private boolean parseOptionalPutArgument(String arg) { + boolean valid = true; + // Given that the order is TAG default overrideableByRestore, we need to parse from the + // opposite direction + switch (arg) { + case "overrideableByRestore": + if (mOverrideableByRestore) { + valid = false; + } else { + mOverrideableByRestore = true; + } + break; + case "default": + if (mMakeDefault || mOverrideableByRestore) { + valid = false; + } else { + mMakeDefault = true; + } + break; + default: // tag + if (mMakeDefault || mOverrideableByRestore || mTag != null) { + valid = false; + } else { + mTag = arg; + } + break; + } + if (!valid) { + Slog.e(LOG_TAG, "parseOptionalPutArgument(" + arg + "): invalid state (" + + "mTag=" + mTag + ", mMakeDefault=" + mMakeDefault + + ", mOverrideableByRestore=" + mOverrideableByRestore + ")"); + } + return valid; + } + List<String> listForUser(IContentProvider provider, int userHandle, String table) { final String callListCommand; if ("system".equals(table)) callListCommand = Settings.CALL_METHOD_LIST_SYSTEM; @@ -351,7 +364,11 @@ final public class SettingsService extends Binder { } void putForUser(IContentProvider provider, int userHandle, final String table, - final String key, final String value, String tag, boolean makeDefault) { + final String key, final String value, String tag, boolean makeDefault, + boolean overrideableByRestore) { + Slog.v(LOG_TAG, "putForUser(userId=" + userHandle + ", table=" + table + ", key=" + key + + ", value=" + value + ", tag=" + tag + ", makeDefault=" + makeDefault + + ", overrideableByRestore=" + overrideableByRestore + ")"); final String callPutCommand; if ("system".equals(table)) { callPutCommand = Settings.CALL_METHOD_PUT_SYSTEM; @@ -377,6 +394,9 @@ final public class SettingsService extends Binder { if (makeDefault) { arg.putBoolean(Settings.CALL_METHOD_MAKE_DEFAULT_KEY, true); } + if (overrideableByRestore) { + arg.putBoolean(Settings.CALL_METHOD_OVERRIDEABLE_BY_RESTORE_KEY, true); + } final AttributionSource attributionSource = new AttributionSource( Binder.getCallingUid(), resolveCallingPackage(), /*attributionTag*/ null); provider.call(attributionSource, Settings.AUTHORITY, @@ -474,10 +494,11 @@ final public class SettingsService extends Binder { pw.println(" Print this help text."); pw.println(" get [--user <USER_ID> | current] NAMESPACE KEY"); pw.println(" Retrieve the current value of KEY."); - pw.println(" put [--user <USER_ID> | current] NAMESPACE KEY VALUE [TAG] [default]"); + pw.println(" put [--user <USER_ID> | current] NAMESPACE KEY VALUE [TAG] [default] [overrideableByRestore]"); pw.println(" Change the contents of KEY to VALUE."); - pw.println(" TAG to associate with the setting."); + pw.println(" TAG to associate with the setting (cannot be default or overrideableByRestore)."); pw.println(" {default} to set as the default, case-insensitive only for global/secure namespace"); + pw.println(" {overrideableByRestore} to let the value be overridden by BackupManager on restore operations"); pw.println(" delete [--user <USER_ID> | current] NAMESPACE KEY"); pw.println(" Delete the entry for KEY."); pw.println(" reset [--user <USER_ID> | current] NAMESPACE {PACKAGE_NAME | RESET_MODE}"); diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 85617bad1a91..70c042cb8eba 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -811,7 +811,7 @@ public class SettingsBackupTest { Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, Settings.Secure.V_TO_U_RESTORE_DENYLIST, Settings.Secure.REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI, - Settings.Secure.REDACT_OTP_NOTIFICATION_IMMEDIATELY); + Settings.Secure.OTP_NOTIFICATION_REDACTION_LOCK_TIME); @Test public void systemSettingsBackedUpOrDenied() { diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index f981545008fa..eb5b22f6c82c 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1370,6 +1370,16 @@ flag { } flag { + name: "media_controls_a11y_colors" + namespace: "systemui" + description: "Color scheme updates for improved a11y" + bug: "378848399" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "output_switcher_redesign" namespace: "systemui" description: "Enables visual update for Media Output Switcher" diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt index 444389fb26ea..fdb07bdbe7f3 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt @@ -155,7 +155,7 @@ constructor( /** [ViewTransitionToken] to be used for storing transitioning view in [transitionRegistry] */ private val transitionToken = if (Flags.decoupleViewControllerInAnimlib()) { - ViewTransitionToken(transitioningView::class.java) + transitionRegistry?.register(transitioningView) } else { null } @@ -164,7 +164,7 @@ constructor( private val ghostedView: View get() = if (Flags.decoupleViewControllerInAnimlib()) { - transitionRegistry?.getView(transitionToken!!) + transitionToken?.let { token -> transitionRegistry?.getView(token) } } else { _ghostedView }!! @@ -186,10 +186,6 @@ constructor( ) } - if (Flags.decoupleViewControllerInAnimlib()) { - transitionRegistry?.register(transitionToken!!, transitioningView) - } - /** Find the first view with a background in [view] and its children. */ fun findBackground(view: View): Drawable? { if (view.background != null) { diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt index af3ca87bf788..280d90de7ace 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt @@ -22,12 +22,12 @@ import android.view.View interface IViewTransitionRegistry { /** - * Registers the transitioning [view] mapped to a [token] + * Registers the transitioning [view] mapped to returned token * - * @param token The token corresponding to the transitioning view * @param view The view undergoing transition + * @return token mapped to the transitioning view */ - fun register(token: ViewTransitionToken, view: View) + fun register(view: View): ViewTransitionToken /** * Unregisters the transitioned view from its corresponding [token] diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt index 86c7f76c6bee..882ff3b61ba9 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt @@ -22,21 +22,21 @@ import java.lang.ref.WeakReference /** * A registry to temporarily store the view being transitioned into a Dialog (using - * [DialogTransitionAnimator]) or an Activity (using [ActivityTransitionAnimator]) + * [DialogTransitionAnimator]) or an Activity (using [ActivityTransitionAnimator]). */ class ViewTransitionRegistry : IViewTransitionRegistry { /** * A map of a unique token to a WeakReference of the View being transitioned. WeakReference * ensures that Views are garbage collected whenever they become eligible and avoid any - * memory leaks + * memory leaks. */ - private val registry by lazy { mutableMapOf<ViewTransitionToken, WeakReference<View>>() } + private val registry by lazy { mutableMapOf<ViewTransitionToken, ViewTransitionInfo>() } /** * A [View.OnAttachStateChangeListener] to be attached to all views stored in the registry to * ensure that views (and their corresponding entry) is automatically removed when the view is - * detached from the Window + * detached from the Window. */ private val listener by lazy { object : View.OnAttachStateChangeListener { @@ -45,74 +45,121 @@ class ViewTransitionRegistry : IViewTransitionRegistry { } override fun onViewDetachedFromWindow(view: View) { - getViewToken(view)?.let { token -> unregister(token) } + // if view is detached from window, remove it from registry irrespective of number + // of reference held by clients/user of this registry + getViewToken(view)?.let { token -> remove(token) } } } } /** - * Creates an entry of a unique "token" mapped to "transitioning view" in the registry + * Creates an entry of a unique token mapped to transitioning [view] in the registry. * - * @param token unique token associated with the transitioning view * @param view view undergoing transitions + * @return unique token mapped to the view being registered */ - override fun register(token: ViewTransitionToken, view: View) { + override fun register(view: View): ViewTransitionToken { + // if view being registered is already present in the registry and has a unique token + // assigned to it, reuse that token + getViewToken(view)?.let { token -> + registry[token]?.let { info -> info.viewRefCount += 1 } + return token + } + // token embedded as a view tag enables to use a single listener for all views + val token = ViewTransitionToken(view::class.java) view.setTag(R.id.tag_view_transition_token, token) view.addOnAttachStateChangeListener(listener) - registry[token] = WeakReference(view) + registry[token] = ViewTransitionInfo(WeakReference(view)) onRegistryUpdate() + + return token } /** - * Removes the entry associated with the unique "token" in the registry + * Unregisters a view mapped to the unique [token] in the registry. This will either remove the + * entry entirely from registry (if the reference count of the associated view reached zero) or + * will decrement the reference count of the associated view in the registry. * * @param token unique token associated with the transitioning view */ override fun unregister(token: ViewTransitionToken) { - registry.remove(token)?.let { - it.get()?.let { view -> + registry[token]?.let { info -> + info.viewRefCount -= 1 + if (info.viewRefCount == 0) { + remove(token) + } + } + } + + /** + * Removes the entry associated with the unique [token] in the registry. + * + * @param token unique token associated with the transitioning view + */ + private fun remove(token: ViewTransitionToken) { + registry.remove(token)?.let { removedInfo -> + removedInfo.viewRef.get()?.let { view -> view.removeOnAttachStateChangeListener(listener) view.setTag(R.id.tag_view_transition_token, null) } - it.clear() + removedInfo.viewRef.clear() onRegistryUpdate() } } /** - * Access a view from registry using unique "token" associated with it + * Access a view from registry using unique [token] associated with it. * WARNING - this returns a StrongReference to the View stored in the registry */ override fun getView(token: ViewTransitionToken): View? { - return registry[token]?.get() + return registry[token]?.viewRef?.get() } /** - * Return token mapped to the [view], if it is present in the registry + * Return token mapped to the [view], if it is present in the registry. * * @param view the transitioning view whose token we are requesting * @return token associated with the [view] if present, else null */ override fun getViewToken(view: View): ViewTransitionToken? { - return (view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken)?.let { token -> - getView(token)?.let { token } + // extract token from the view if it is embedded inside it as a tag + val token = view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken + + // this should never really happen, but if token embedded inside the view as tag, doesn't + // point to a valid view in the registry, remove that token (tag) from the view and registry + if (token != null && getView(token) == null) { + view.setTag(R.id.tag_view_transition_token, null) + remove(token) + return null } + + return token } - /** Event call to run on registry update (on both [register] and [unregister]) */ + /** Event call to run on registry update (on both [register] and [unregister]). */ override fun onRegistryUpdate() { emitCountForTrace() } /** * Utility function to emit number of non-null views in the registry whenever the registry is - * updated (via [register] or [unregister]) + * updated (via [register] or [unregister]). */ private fun emitCountForTrace() { Trace.setCounter("transition_registry_view_count", registry.count().toLong()) } + /** Information associated with each transitioning view in the registry. */ + private data class ViewTransitionInfo( + + /** View being transitioned */ + val viewRef: WeakReference<View>, + + /** Count of clients (users of this registry) referencing same transitioning view */ + var viewRefCount: Int = 1 + ) + companion object { val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ViewTransitionRegistry() } } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/ShadeDisplayAwareDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/ShadeDisplayAwareDetector.kt index 2a27a3033cf9..6eafb9f60b9e 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/ShadeDisplayAwareDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/ShadeDisplayAwareDetector.kt @@ -29,6 +29,14 @@ import com.intellij.psi.PsiParameter import org.jetbrains.uast.UClass import org.jetbrains.uast.getContainingUFile +/** + * Lint check to ensure that when including a Context or Context-dependent argument in + * shade-relevant packages, the argument has the @ShadeDisplayAware annotation. + * + * This is to ensure that Context-dependent components correctly handle Configuration changes when + * the shade is moved to a different display. @ShadeDisplayAware-annotated components will update + * accordingly to reflect the new display. + */ class ShadeDisplayAwareDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes() = listOf(UClass::class.java) @@ -38,8 +46,8 @@ class ShadeDisplayAwareDetector : Detector(), SourceCodeScanner { for (constructor in node.constructors) { // Visit all injected constructors in shade-relevant packages if (!constructor.hasAnnotation(INJECT_ANNOTATION)) continue - if (!isInRelevantShadePackage(node)) continue - if (IGNORED_PACKAGES.contains(node.qualifiedName)) continue + if (!isInRelevantShadePackage(node.getContainingUFile()?.packageName)) continue + if (IGNORED_CLASSES.contains(node.qualifiedName)) continue for (parameter in constructor.parameterList.parameters) { if (parameter.shouldReport()) { @@ -84,24 +92,19 @@ class ShadeDisplayAwareDetector : Detector(), SourceCodeScanner { CONFIG_INTERACTOR, ) - private val CONFIG_CLASSES = setOf(CONFIG_STATE, CONFIG_CONTROLLER, CONFIG_INTERACTOR) - private val SHADE_WINDOW_PACKAGES = listOf( "com.android.systemui.biometrics", "com.android.systemui.bouncer", "com.android.systemui.keyboard.docking.ui.viewmodel", + "com.android.systemui.media.controls.ui.controller", "com.android.systemui.qs", "com.android.systemui.shade", - "com.android.systemui.statusbar.notification", + "com.android.systemui.statusbar.lockscreen", "com.android.systemui.unfold.domain.interactor", ) - private val IGNORED_PACKAGES = - setOf( - "com.android.systemui.biometrics.UdfpsController", - "com.android.systemui.qs.customize.TileAdapter", - ) + private val IGNORED_CLASSES = setOf("com.android.systemui.statusbar.phone.SystemUIDialog") private fun PsiParameter.shouldReport(): Boolean { val className = type.canonicalText @@ -116,8 +119,7 @@ class ShadeDisplayAwareDetector : Detector(), SourceCodeScanner { return true } - private fun isInRelevantShadePackage(node: UClass): Boolean { - val packageName = node.getContainingUFile()?.packageName + fun isInRelevantShadePackage(packageName: String?): Boolean { if (packageName.isNullOrBlank()) return false return SHADE_WINDOW_PACKAGES.any { relevantPackage -> packageName.startsWith(relevantPackage) diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/ShadeDisplayAwareDialogDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/ShadeDisplayAwareDialogDetector.kt new file mode 100644 index 000000000000..4cd5d89ea919 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/ShadeDisplayAwareDialogDetector.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.systemui.lint + +import com.android.internal.systemui.lint.ShadeDisplayAwareDetector.Companion.isInRelevantShadePackage +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.getContainingUClass +import org.jetbrains.uast.getContainingUFile + +/** + * Lint check to ensure that when creating dialogs shade-relevant packages, the correct Context is + * provided. + * + * This is to ensure that the dialog is created with the correct context when the shade is moved to + * a different display. When the shade is moved, the configuration might change, and only + * `@ShadeDisplayAware`-annotated components will update accordingly to reflect the new display. + * + * Example: + * ```kotlin + * class ExampleClass + * @Inject + * constructor(private val contextInteractor: ShadeDialogContextInteractor) { + * + * fun showDialog() { + * val dialog = systemUIDialogFactory.create(delegate, contextInteractor.context) + * dialog.show() + * } + * } + * ``` + */ +// TODO: b/396066687 - update linter after refactoring to use ShadeDialogFactory +class ShadeDisplayAwareDialogDetector : Detector(), SourceCodeScanner { + override fun getApplicableMethodNames(): List<String> = listOf(CREATE_METHOD) + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + if (!isInRelevantShadePackage(node.getContainingUFile()?.packageName)) return + if (!context.evaluator.isMemberInClass(method, SYSUI_DIALOG_FACTORY)) return + val contextArg = + node.valueArguments.find { + it.getExpressionType()?.canonicalText == "android.content.Context" + } + if (contextArg == null) { + context.report( + issue = ISSUE, + scope = node, + location = context.getNameLocation(node), + message = + "SystemUIDialog.Factory#create requires a Context that accounts for the " + + "shade's display. Use create(shadeDialogContextInteractor.getContext()) " + + "or create(shadeDialogContextInteractor.context) to provide the correct Context.", + ) + } else { + val isProvidedByContextInteractor = + contextArg.tryResolveUDeclaration()?.getContainingUClass()?.qualifiedName == + SHADE_DIALOG_CONTEXT_INTERACTOR + + if (!isProvidedByContextInteractor) { + context.report( + issue = ISSUE, + scope = contextArg, + location = context.getNameLocation(contextArg), + message = + "In shade-relevant packages, SystemUIDialog.Factory#create must be called " + + "with the Context directly from ShadeDialogContextInteractor " + + "(ShadeDialogContextInteractor.context or getContext()). " + + "Avoid intermediate variables or function calls. This direct usage " + + "is required to ensure proper shade display handling.", + ) + } + } + } + + companion object { + private const val CREATE_METHOD = "create" + private const val SYSUI_DIALOG_FACTORY = + "com.android.systemui.statusbar.phone.SystemUIDialog.Factory" + private const val SHADE_DIALOG_CONTEXT_INTERACTOR = + "com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor" + + @JvmField + val ISSUE: Issue = + Issue.create( + id = "ShadeDisplayAwareDialogChecker", + briefDescription = "Checking for shade display aware context when creating dialogs", + explanation = + """ + Dialogs created by the notification shade must use a special Context to appear + on the correct display, especially when the shade is not on the default display. + """ + .trimIndent(), + category = Category.CORRECTNESS, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + ShadeDisplayAwareDialogDetector::class.java, + Scope.JAVA_FILE_SCOPE, + ), + ) + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index 6d18f9377806..adb311610587 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -47,6 +47,7 @@ class SystemUIIssueRegistry : IssueRegistry() { TestFunctionNameViolationDetector.ISSUE, MissingApacheLicenseDetector.ISSUE, ShadeDisplayAwareDetector.ISSUE, + ShadeDisplayAwareDialogDetector.ISSUE, RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING, RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR, ) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/ShadeDisplayAwareDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/ShadeDisplayAwareDetectorTest.kt index 638d7cb7ee58..f8f8b40c4c01 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/ShadeDisplayAwareDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/ShadeDisplayAwareDetectorTest.kt @@ -410,34 +410,6 @@ class ShadeDisplayAwareDetectorTest : SystemUILintDetectorTest() { .expectClean() } - @Test - fun injectedConstructor_inExemptPackage_withRelevantParameter_withoutAnnotation() { - lint() - .files( - TestFiles.java( - """ - package com.android.systemui.qs.customize; - - import javax.inject.Inject; - import com.android.systemui.qs.dagger.QSThemedContext; - import android.content.Context; - - public class TileAdapter { - @Inject - public TileAdapter(@QSThemedContext Context context) {} - } - """ - .trimIndent() - ), - *androidStubs, - *otherStubs, - ) - .issues(ShadeDisplayAwareDetector.ISSUE) - .testModes(TestMode.DEFAULT) - .run() - .expectClean() - } - private fun errorMsgString(lineNumber: Int, className: String) = "src/com/android/systemui/shade/example/ExampleClass.kt:$lineNumber: Error: UI elements of " + "the shade window should use ShadeDisplayAware-annotated $className, as the shade " + diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/ShadeDisplayAwareDialogDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/ShadeDisplayAwareDialogDetectorTest.kt new file mode 100644 index 000000000000..86cdcdcf9644 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/ShadeDisplayAwareDialogDetectorTest.kt @@ -0,0 +1,541 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.checks.infrastructure.TestMode +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +class ShadeDisplayAwareDialogDetectorTest : SystemUILintDetectorTest() { + override fun getDetector(): Detector = ShadeDisplayAwareDialogDetector() + + override fun getIssues(): List<Issue> = listOf(ShadeDisplayAwareDialogDetector.ISSUE) + + private val injectStub: TestFile = + kotlin( + """ + package javax.inject + @Retention(AnnotationRetention.RUNTIME) annotation class Inject + """ + ) + .indented() + private val shadeDisplayAwareStub: TestFile = + kotlin( + """ + package com.android.systemui.shade + @Retention(AnnotationRetention.RUNTIME) annotation class ShadeDisplayAware + """ + ) + .indented() + private val mainStub: TestFile = + kotlin( + """ + package com.android.systemui.dagger.qualifiers + + @Retention(AnnotationRetention.RUNTIME) annotation class Main + """ + ) + .indented() + private val dialogContextStub: TestFile = + kotlin( + """ + package com.android.systemui.shade.domain.interactor + + import android.content.Context + + interface ShadeDialogContextInteractor { + val context: Context + } + """ + ) + .indented() + private val delegateStub: TestFile = + java( + """ + package com.android.systemui.statusbar.phone; + + public interface Delegate { } + """ + .trimIndent() + ) + private val sysuiDialogStub: TestFile = + java( + """ + package com.android.systemui.statusbar.phone; + + import android.content.Context; + public class SystemUIDialog { + public SystemUIDialog(int id) { } + + public static class Factory { + public SystemUIDialog create() { + return new SystemUIDialog(); + } + + public SystemUIDialog create(Context context) { + return new SystemUIDialog(); + } + + public SystemUIDialog create(Delegate delegate, Context context) { + return new SystemUIDialog(); + } + + public SystemUIDialog create(Delegate delegate, Context context, + boolean shouldAcsdDismissDialog) { + return new SystemUIDialog(); + } + + public SystemUIDialog create(Delegate delegate, Context context, int theme) { + return new SystemUIDialog(); + } + + public SystemUIDialog create(Delegate delegate) { + return new SystemUIDialog(); + } + } + } + """ + ) + .indented() + + private val otherStubs = + arrayOf( + injectStub, + shadeDisplayAwareStub, + mainStub, + delegateStub, + sysuiDialogStub, + dialogContextStub, + ) + + @Test + fun create_noArguments() { + lint() + .files( + TestFiles.kotlin( + """ + package com.android.systemui.shade.example + import javax.inject.Inject + import android.content.Context + import com.android.systemui.statusbar.phone.SystemUIDialog + + class ExampleClass + @Inject + constructor(private val systemUIDialogFactory: SystemUIDialog.Factory) { + + fun showDialog() { + val dialog = systemUIDialogFactory.create() + dialog.show() + } + } + """ + .trimIndent() + ), + *androidStubs, + *otherStubs, + ) + .issues(ShadeDisplayAwareDialogDetector.ISSUE) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(1) + .expectContains( + "SystemUIDialog.Factory#create requires a Context that accounts for the " + + "shade's display. Use create(shadeDialogContextInteractor.getContext()) " + + "or create(shadeDialogContextInteractor.context) to provide the correct Context." + ) + .expectContains("[ShadeDisplayAwareDialogChecker]") + .expectContains("0 errors, 1 warning") + } + + @Test + fun create_UnannotatedContext() { + lint() + .files( + TestFiles.kotlin( + """ + package com.android.systemui.shade.example + import javax.inject.Inject + import android.content.Context + import com.android.systemui.statusbar.phone.SystemUIDialog + class ExampleClass + @Inject + constructor( + private val context: Context, + private val systemUIDialogFactory: SystemUIDialog.Factory + ) { + + fun showDialog() { + val dialog = systemUIDialogFactory.create(context) + dialog.show() + } + } + """ + .trimIndent() + ), + *androidStubs, + *otherStubs, + ) + .issues(ShadeDisplayAwareDialogDetector.ISSUE) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(1) + .expectContains( + "In shade-relevant packages, SystemUIDialog.Factory#create must be called " + + "with the Context directly from ShadeDialogContextInteractor " + + "(ShadeDialogContextInteractor.context or getContext()). " + + "Avoid intermediate variables or function calls. This direct usage " + + "is required to ensure proper shade display handling." + ) + .expectContains("[ShadeDisplayAwareDialogChecker]") + .expectContains("0 errors, 1 warning") + } + + @Test + fun create_annotatedContext() { + lint() + .files( + TestFiles.kotlin( + """ + package com.android.systemui.shade.example + import javax.inject.Inject + import android.content.Context + import com.android.systemui.statusbar.phone.SystemUIDialog + import com.android.systemui.shade.ShadeDisplayAware + class ExampleClass + @Inject + constructor( + @ShadeDisplayAware private val context: Context, + private val systemUIDialogFactory: SystemUIDialog.Factory + ) { + + fun showDialog() { + val dialog = systemUIDialogFactory.create(context) + dialog.show() + } + } + """ + .trimIndent() + ), + *androidStubs, + *otherStubs, + ) + .issues(ShadeDisplayAwareDialogDetector.ISSUE) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(1) + .expectContains( + "In shade-relevant packages, SystemUIDialog.Factory#create must be called " + + "with the Context directly from ShadeDialogContextInteractor " + + "(ShadeDialogContextInteractor.context or getContext()). " + + "Avoid intermediate variables or function calls. This direct usage " + + "is required to ensure proper shade display handling." + ) + .expectContains("[ShadeDisplayAwareDialogChecker]") + .expectContains("0 errors, 1 warning") + } + + @Test + fun create_LocalVariable() { + lint() + .files( + TestFiles.kotlin( + """ + package com.android.systemui.shade.example + import javax.inject.Inject + import com.android.systemui.statusbar.phone.SystemUIDialog + import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor + + class ExampleClass + @Inject + constructor( + private val contextInteractor: ShadeDialogContextInteractor, + private val systemUIDialogFactory: SystemUIDialog.Factory + ) { + + fun showDialog() { + val context2 = contextInteractor.context + val dialog = systemUIDialogFactory.create(context2) + dialog.show() + } + } + """ + .trimIndent() + ), + *androidStubs, + *otherStubs, + ) + .issues(ShadeDisplayAwareDialogDetector.ISSUE) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(1) + .expectContains( + "In shade-relevant packages, SystemUIDialog.Factory#create must be called " + + "with the Context directly from ShadeDialogContextInteractor " + + "(ShadeDialogContextInteractor.context or getContext()). " + + "Avoid intermediate variables or function calls. This direct usage " + + "is required to ensure proper shade display handling." + ) + .expectContains("[ShadeDisplayAwareDialogChecker]") + .expectContains("0 errors, 1 warning") + } + + @Test + fun create_delegate_UnannotatedContext() { + lint() + .files( + TestFiles.kotlin( + """ + package com.android.systemui.shade.example + import javax.inject.Inject + import android.content.Context + import com.android.systemui.statusbar.phone.SystemUIDialog + import com.android.systemui.statusbar.phone.Delegate + class ExampleClass + @Inject + constructor( + private val context: Context, + private val delegate: Delegate, + private val systemUIDialogFactory: SystemUIDialog.Factory + ) { + + fun showDialog() { + val dialog = systemUIDialogFactory.create(delegate, context) + dialog.show() + } + } + """ + .trimIndent() + ), + *androidStubs, + *otherStubs, + ) + .issues(ShadeDisplayAwareDialogDetector.ISSUE) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(1) + .expectContains( + "In shade-relevant packages, SystemUIDialog.Factory#create must be called " + + "with the Context directly from ShadeDialogContextInteractor " + + "(ShadeDialogContextInteractor.context or getContext()). " + + "Avoid intermediate variables or function calls. This direct usage " + + "is required to ensure proper shade display handling." + ) + .expectContains("[ShadeDisplayAwareDialogChecker]") + .expectContains("0 errors, 1 warning") + } + + @Test + fun create_delegate_Context() { + lint() + .files( + TestFiles.kotlin( + """ + package com.android.systemui.shade.example + import javax.inject.Inject + import android.content.Context + import com.android.systemui.statusbar.phone.SystemUIDialog + import com.android.systemui.statusbar.phone.Delegate + import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor + + class ExampleClass + @Inject + constructor( + private val delegate: Delegate, + private val contextInteractor: ShadeDialogContextInteractor, + private val systemUIDialogFactory: SystemUIDialog.Factory + ) { + + fun showDialog() { + val dialog = systemUIDialogFactory.create(delegate, contextInteractor.context) + dialog.show() + } + } + """ + .trimIndent() + ), + *androidStubs, + *otherStubs, + ) + .issues(ShadeDisplayAwareDialogDetector.ISSUE) + .testModes(TestMode.DEFAULT) + .run() + .expectClean() + } + + @Test + fun create_Delegate_Context_Boolean() { + lint() + .files( + TestFiles.kotlin( + """ + package com.android.systemui.shade.example + import javax.inject.Inject + import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor + import com.android.systemui.statusbar.phone.SystemUIDialog + import com.android.systemui.statusbar.phone.Delegate + + class ExampleClass + @Inject + constructor( + private val delegate: Delegate, + private val contextInteractor: ShadeDialogContextInteractor, + private val systemUIDialogFactory: SystemUIDialog.Factory + ) { + + fun showDialog() { + val dialog = systemUIDialogFactory.create(delegate, contextInteractor.context, true) + dialog.show() + } + } + """ + .trimIndent() + ), + *androidStubs, + *otherStubs, + ) + .issues(ShadeDisplayAwareDialogDetector.ISSUE) + .testModes(TestMode.DEFAULT) + .run() + .expectClean() + } + + @Test + fun create_Delegate_UnannotatedContext_Int() { + lint() + .files( + TestFiles.kotlin( + """ + package com.android.systemui.shade.example + import javax.inject.Inject + import android.content.Context + import com.android.systemui.statusbar.phone.SystemUIDialog + import com.android.systemui.statusbar.phone.Delegate + class ExampleClass + @Inject + constructor( + private val context: Context, + private val delegate: Delegate, + private val systemUIDialogFactory: SystemUIDialog.Factory + ) { + + fun showDialog() { + val dialog = systemUIDialogFactory.create(delegate, context, 0) + dialog.show() + } + } + """ + .trimIndent() + ), + *androidStubs, + *otherStubs, + ) + .issues(ShadeDisplayAwareDialogDetector.ISSUE) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(1) + .expectContains( + "In shade-relevant packages, SystemUIDialog.Factory#create must be called " + + "with the Context directly from ShadeDialogContextInteractor " + + "(ShadeDialogContextInteractor.context or getContext()). " + + "Avoid intermediate variables or function calls. This direct usage " + + "is required to ensure proper shade display handling." + ) + .expectContains("[ShadeDisplayAwareDialogChecker]") + .expectContains("0 errors, 1 warning") + } + + @Test + fun create_Delegate_Context_Int() { + lint() + .files( + TestFiles.kotlin( + """ + package com.android.systemui.shade.example + import javax.inject.Inject + import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor + import com.android.systemui.statusbar.phone.SystemUIDialog + import com.android.systemui.statusbar.phone.Delegate + + class ExampleClass + @Inject + constructor( + private val delegate: Delegate, + private val contextInteractor: ShadeDialogContextInteractor, + private val systemUIDialogFactory: SystemUIDialog.Factory + ) { + + fun showDialog() { + val dialog = systemUIDialogFactory.create(delegate, contextInteractor.context, 0) + dialog.show() + } + } + """ + .trimIndent() + ), + *androidStubs, + *otherStubs, + ) + .issues(ShadeDisplayAwareDialogDetector.ISSUE) + .testModes(TestMode.DEFAULT) + .run() + .expectClean() + } + + @Test + fun create_Delegate() { + lint() + .files( + TestFiles.kotlin( + """ + package com.android.systemui.shade.example + import javax.inject.Inject + import com.android.systemui.statusbar.phone.Delegate + import com.android.systemui.statusbar.phone.SystemUIDialog + + class ExampleClass + @Inject + constructor( + private val delegate: Delegate, + private val systemUIDialogFactory: SystemUIDialog.Factory + ) { + + fun showDialog() { + val dialog = systemUIDialogFactory.create(delegate) + dialog.show() + } + } + """ + .trimIndent() + ), + *androidStubs, + *otherStubs, + ) + .issues(ShadeDisplayAwareDialogDetector.ISSUE) + .testModes(TestMode.DEFAULT) + .run() + .expectWarningCount(1) + .expectContains( + "SystemUIDialog.Factory#create requires a Context that accounts for the " + + "shade's display. Use create(shadeDialogContextInteractor.getContext()) " + + "or create(shadeDialogContextInteractor.context) to provide the correct Context." + ) + .expectContains("[ShadeDisplayAwareDialogChecker]") + .expectContains("0 errors, 1 warning") + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt index 297995becfb2..7cd6c6b47f2a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -124,7 +124,6 @@ constructor( SmallClock( burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmallClockTopChanged, - modifier = Modifier.fillMaxWidth(), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index da0b8ac7bda3..0f1cb409439f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -50,6 +50,8 @@ import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.media.controls.ui.composable.MediaCarousel +import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.COLLAPSED import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.flags.QsDetailedView @@ -104,7 +106,10 @@ constructor( } val quickSettingsContainerViewModel = rememberViewModel("QuickSettingsShadeOverlayContainer") { - quickSettingsContainerViewModelFactory.create(supportsBrightnessMirroring = true) + quickSettingsContainerViewModelFactory.create( + supportsBrightnessMirroring = true, + expansion = COLLAPSED, + ) } val hunPlaceholderViewModel = rememberViewModel("QuickSettingsShadeOverlayPlaceholder") { @@ -232,12 +237,20 @@ fun ContentScope.QuickSettingsLayout( Toolbar( modifier = Modifier.fillMaxWidth().requiredHeight(QuickSettingsShade.Dimensions.ToolbarHeight), - toolbarViewModelFactory = viewModel.toolbarViewModelFactory, + viewModel = viewModel.toolbarViewModel, ) Column( verticalArrangement = Arrangement.spacedBy(QuickSettingsShade.Dimensions.Padding), modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), ) { + MediaCarousel( + isVisible = viewModel.showMedia, + mediaHost = viewModel.mediaHost, + carouselController = viewModel.mediaCarouselController, + usingCollapsedLandscapeMedia = true, + modifier = Modifier.padding(horizontal = QuickSettingsShade.Dimensions.Padding), + ) + BrightnessSliderContainer( viewModel = viewModel.brightnessSliderViewModel, containerColor = OverlayShade.Colors.PanelBackground, @@ -245,6 +258,7 @@ fun ContentScope.QuickSettingsLayout( Modifier.fillMaxWidth() .height(QuickSettingsShade.Dimensions.BrightnessSliderHeight), ) + Box { GridAnchor() TileGrid( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt index 60eaa28e3822..b9aca25e1675 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt @@ -21,7 +21,6 @@ import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey -import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.observableTransitionState import com.android.systemui.scene.shared.model.SceneDataSource import kotlinx.coroutines.CoroutineScope @@ -106,6 +105,6 @@ class SceneTransitionLayoutDataSource( } override fun freezeAndAnimateToCurrentState() { - (state.transitionState as? TransitionState.Transition)?.freezeAndAnimateToCurrentState() + state.currentTransition?.freezeAndAnimateToCurrentState() } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index cdb1e2e53b09..7e7b6297406e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -54,6 +54,7 @@ import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.res.R +import androidx.compose.ui.unit.Dp /** Renders a lightweight shade UI container, as an overlay. */ @Composable @@ -202,10 +203,15 @@ object OverlayShade { } object Dimensions { - val PanelCornerRadius = 46.dp + val PanelCornerRadius: Dp + @Composable + @ReadOnlyComposable get() = + dimensionResource(R.dimen.overlay_shade_panel_shape_radius) } object Shapes { - val RoundedCornerPanel = RoundedCornerShape(Dimensions.PanelCornerRadius) + val RoundedCornerPanel: RoundedCornerShape + @Composable + @ReadOnlyComposable get() = RoundedCornerShape(Dimensions.PanelCornerRadius) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 02de78bc84ce..db9035b1635b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -25,8 +25,8 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer @@ -43,10 +43,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer @@ -67,6 +65,7 @@ import com.android.compose.animation.scene.ValueKey import com.android.compose.animation.scene.animateElementFloatAsState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.thenIf +import com.android.compose.theme.colorAttr import com.android.settingslib.Utils import com.android.systemui.battery.BatteryMeterView import com.android.systemui.battery.BatteryMeterViewController @@ -77,21 +76,18 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.chipBackground -import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.chipHighlighted import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.onScrimDim +import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.ChipPaddingHorizontal +import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.ChipPaddingVertical import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.CollapsedHeight import com.android.systemui.shade.ui.composable.ShadeHeader.Values.ClockScale import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder -import com.android.systemui.statusbar.phone.NotificationIconContainer import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel import com.android.systemui.statusbar.policy.Clock -import kotlinx.coroutines.launch object ShadeHeader { object Elements { @@ -110,6 +106,8 @@ object ShadeHeader { object Dimensions { val CollapsedHeight = 48.dp val ExpandedHeight = 120.dp + val ChipPaddingHorizontal = 6.dp + val ChipPaddingVertical = 4.dp } object Colors { @@ -118,12 +116,6 @@ object ShadeHeader { val ColorScheme.onScrimDim: Color get() = Color.DarkGray - - val ColorScheme.chipBackground: Color - get() = Color.DarkGray - - val ColorScheme.chipHighlighted: Color - get() = Color.LightGray } object TestTags { @@ -149,9 +141,6 @@ fun ContentScope.CollapsedShadeHeader( } } - val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() - val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle() - val isShadeLayoutWide = viewModel.isShadeLayoutWide val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() @@ -167,9 +156,9 @@ fun ContentScope.CollapsedShadeHeader( ) { Clock(scale = 1f, onClick = viewModel::onClockClicked) VariableDayDate( - longerDateText = longerDateText, - shorterDateText = shorterDateText, - chipHighlight = viewModel.notificationsChipHighlight, + longerDateText = viewModel.longerDateText, + shorterDateText = viewModel.shorterDateText, + textColor = colorAttr(android.R.attr.textColorPrimary), modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentStart), ) } @@ -229,8 +218,6 @@ fun ContentScope.ExpandedShadeHeader( derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) } } - val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() - val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle() val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) { @@ -269,9 +256,9 @@ fun ContentScope.ExpandedShadeHeader( modifier = Modifier.element(ShadeHeader.Elements.ExpandedContent), ) { VariableDayDate( - longerDateText = longerDateText, - shorterDateText = shorterDateText, - chipHighlight = viewModel.notificationsChipHighlight, + longerDateText = viewModel.longerDateText, + shorterDateText = viewModel.shorterDateText, + textColor = colorAttr(android.R.attr.textColorPrimary), modifier = Modifier.widthIn(max = 90.dp), ) Spacer(modifier = Modifier.weight(1f)) @@ -316,6 +303,7 @@ fun ContentScope.OverlayShadeHeader( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = horizontalPadding), ) { + val chipHighlight = viewModel.notificationsChipHighlight if (isShadeLayoutWide) { Clock( scale = 1f, @@ -324,28 +312,15 @@ fun ContentScope.OverlayShadeHeader( ) Spacer(modifier = Modifier.width(5.dp)) } - val chipHighlight = viewModel.notificationsChipHighlight - NotificationIconChip( - chipHighlight = chipHighlight, + NotificationsChip( onClick = viewModel::onNotificationIconChipClicked, + backgroundColor = chipHighlight.backgroundColor(MaterialTheme.colorScheme), ) { - if (isShadeLayoutWide) { - NotificationIcons( - chipHighlight = chipHighlight, - notificationIconContainerStatusBarViewBinder = - viewModel.notificationIconContainerStatusBarViewBinder, - modifier = Modifier.width(IntrinsicSize.Min).height(20.dp), - ) - } else { - val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle() - val shorterDateText by - viewModel.shorterDateText.collectAsStateWithLifecycle() - VariableDayDate( - longerDateText = longerDateText, - shorterDateText = shorterDateText, - chipHighlight = viewModel.notificationsChipHighlight, - ) - } + VariableDayDate( + longerDateText = viewModel.longerDateText, + shorterDateText = viewModel.shorterDateText, + textColor = chipHighlight.foregroundColor(MaterialTheme.colorScheme), + ) } } }, @@ -357,14 +332,13 @@ fun ContentScope.OverlayShadeHeader( ) { val chipHighlight = viewModel.quickSettingsChipHighlight SystemIconChip( - chipHighlight = chipHighlight, + backgroundColor = chipHighlight.backgroundColor(MaterialTheme.colorScheme), onClick = viewModel::onSystemIconChipClicked, ) { StatusIcons( viewModel = viewModel, useExpandedFormat = false, modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false), - chipHighlight = chipHighlight, ) BatteryIcon( createBatteryMeterViewController = @@ -534,6 +508,7 @@ private fun BatteryIcon( batteryIcon.setPercentShowMode( if (useExpandedFormat) BatteryMeterView.MODE_ESTIMATE else BatteryMeterView.MODE_ON ) + // TODO(b/397223606): Get the actual spec for this. if (chipHighlight is HeaderChipHighlight.Strong) { batteryIcon.updateColors(primaryColor, inverseColor, inverseColor) } else if (chipHighlight is HeaderChipHighlight.Weak) { @@ -546,11 +521,8 @@ private fun BatteryIcon( @Composable private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) { - Row(modifier = modifier) { - val subIds by viewModel.mobileSubIds.collectAsStateWithLifecycle() - - for (subId in subIds) { - Spacer(modifier = Modifier.width(5.dp)) + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(5.dp)) { + for (subId in viewModel.mobileSubIds) { AndroidView( factory = { context -> ModernShadeCarrierGroupMobileView.constructAndBind( @@ -571,36 +543,10 @@ private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifie } @Composable -private fun NotificationIcons( - chipHighlight: HeaderChipHighlight, - notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder, - modifier: Modifier = Modifier, -) { - val scope = rememberCoroutineScope() - - AndroidView( - factory = { context -> - NotificationIconContainer(context, null).also { view -> - view.setOverrideIconColor(true) - scope.launch { - notificationIconContainerStatusBarViewBinder.bindWhileAttached( - view = view, - displayId = context.displayId, - ) - } - } - }, - update = { it.setUseInverseOverrideIconColor(chipHighlight is HeaderChipHighlight.Strong) }, - modifier = modifier, - ) -} - -@Composable private fun ContentScope.StatusIcons( viewModel: ShadeHeaderViewModel, useExpandedFormat: Boolean, modifier: Modifier = Modifier, - chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None, ) { val localContext = LocalContext.current val themedContext = @@ -628,6 +574,8 @@ private fun ContentScope.StatusIcons( viewModel.createTintedIconManager(iconContainer, StatusBarLocation.QS) } + val chipHighlight = viewModel.quickSettingsChipHighlight + AndroidView( factory = { context -> iconManager.setTint(primaryColor, inverseColor) @@ -664,6 +612,7 @@ private fun ContentScope.StatusIcons( iconContainer.removeIgnoredSlot(locationSlot) } + // TODO(b/397223606): Get the actual spec for this. if (chipHighlight is HeaderChipHighlight.Strong) { iconManager.setTint(inverseColor, primaryColor) } else if (chipHighlight is HeaderChipHighlight.Weak) { @@ -675,62 +624,49 @@ private fun ContentScope.StatusIcons( } @Composable -private fun NotificationIconChip( - chipHighlight: HeaderChipHighlight, +private fun NotificationsChip( onClick: () -> Unit, modifier: Modifier = Modifier, - content: @Composable RowScope.() -> Unit, + backgroundColor: Color = Color.Unspecified, + content: @Composable BoxScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } - Box(modifier = modifier) { - Row( - modifier = - Modifier.align(Alignment.CenterStart) - .clickable( - interactionSource = interactionSource, - indication = null, - onClick = { onClick() }, - ) - .clip(RoundedCornerShape(25.dp)) - .background( - if (chipHighlight is HeaderChipHighlight.Strong) - MaterialTheme.colorScheme.chipHighlighted - else MaterialTheme.colorScheme.chipBackground - ) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - content() - } + Box( + modifier = + modifier + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onClick, + ) + .background(backgroundColor, RoundedCornerShape(25.dp)) + .padding(horizontal = ChipPaddingHorizontal, vertical = ChipPaddingVertical) + ) { + content() } } @Composable private fun SystemIconChip( modifier: Modifier = Modifier, - chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None, + backgroundColor: Color = Color.Unspecified, onClick: (() -> Unit)? = null, content: @Composable RowScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() val hoverModifier = - Modifier.clip(RoundedCornerShape(CollapsedHeight / 4)) - .background(MaterialTheme.colorScheme.onScrimDim) - val backgroundColor = - if (chipHighlight is HeaderChipHighlight.Strong) MaterialTheme.colorScheme.chipHighlighted - else MaterialTheme.colorScheme.chipBackground + with(MaterialTheme.colorScheme) { + Modifier.background(onScrimDim, RoundedCornerShape(CollapsedHeight / 4)) + } Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier - .thenIf(chipHighlight !is HeaderChipHighlight.None) { - Modifier.graphicsLayer { - shape = RoundedCornerShape(25.dp) - clip = true - } - .background(backgroundColor) - .padding(horizontal = 8.dp, vertical = 4.dp) + .thenIf(backgroundColor != Color.Unspecified) { + Modifier.background(backgroundColor, RoundedCornerShape(25.dp)) + .padding(horizontal = ChipPaddingHorizontal, vertical = ChipPaddingVertical) } .thenIf(onClick != null) { Modifier.clickable( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt index 64aada52626b..8fbd0519cbdf 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt @@ -4,22 +4,16 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.Layout -import com.android.compose.theme.colorAttr -import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight @Composable fun VariableDayDate( longerDateText: String, shorterDateText: String, - chipHighlight: HeaderChipHighlight, + textColor: Color, modifier: Modifier = Modifier, ) { - val textColor = - if (chipHighlight is HeaderChipHighlight.Strong) - colorAttr(android.R.attr.textColorPrimaryInverse) - else colorAttr(android.R.attr.textColorPrimary) - Layout( contents = listOf( diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index 8317aa39ef2b..54be4d81ea06 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -215,12 +215,7 @@ open class SimpleDigitalClockTextView( ) } - setInterpolatedViewBounds( - getInterpolatedTextBounds(), - widthMeasureSpec, - heightMeasureSpec, - force = true, - ) + setInterpolatedViewBounds(getInterpolatedTextBounds(), widthMeasureSpec, heightMeasureSpec) } override fun onDraw(canvas: Canvas) { @@ -388,7 +383,6 @@ open class SimpleDigitalClockTextView( interpBounds: Rect, widthMeasureSpec: Int = measuredWidthAndState, heightMeasureSpec: Int = measuredHeightAndState, - force: Boolean = false, ) { val heightMode = MeasureSpec.getMode(heightMeasureSpec) val widthMode = MeasureSpec.getMode(widthMeasureSpec) @@ -415,10 +409,7 @@ open class SimpleDigitalClockTextView( ) } - if (force || widthSpec != measuredWidthAndState || heightSpec != measuredHeightAndState) { - setMeasuredDimension(widthSpec, heightSpec) - parent?.requestLayout() - } + setMeasuredDimension(widthSpec, heightSpec) } private fun updateXTranslation(inPoint: Point, interpolatedTextBounds: Rect): Point { diff --git a/packages/SystemUI/lint-baseline.xml b/packages/SystemUI/lint-baseline.xml index b0963d352990..993932c8a041 100644 --- a/packages/SystemUI/lint-baseline.xml +++ b/packages/SystemUI/lint-baseline.xml @@ -32784,4 +32784,15 @@ column="23"/> </issue> + <issue + id="ShadeDisplayAwareContextChecker" + message="UI elements of the shade window should use ShadeDisplayAware-annotated Context, as the shade might move between windows, and only @ShadeDisplayAware resources are updated with the new configuration correctly. Failures to do so might result in wrong dimensions for shade window classes (e.g. using the wrong density or theme). If the usage of Context is not related to display specific configuration or UI, then there is technically no need to use the annotation, and you can annotate the class with @SuppressLint("ShadeDisplayAwareContextChecker")/@Suppress("ShadeDisplayAwareContextChecker")" + errorLine1=" @QSThemedContext Context context," + errorLine2=" ~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java" + line="126" + column="38"/> + </issue> + </issues> diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/EmergencyButtonControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/EmergencyButtonControllerTest.kt index c42e25b20e0d..d046ad114fa5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/EmergencyButtonControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/EmergencyButtonControllerTest.kt @@ -103,7 +103,7 @@ class EmergencyButtonControllerTest : SysuiTestCase() { fun testUpdateEmergencyButton() { Mockito.`when`(telecomManager.isInCall).thenReturn(true) Mockito.`when`(lockPatternUtils.isSecure(anyInt())).thenReturn(true) - Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) + Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) .thenReturn(true) underTest.updateEmergencyCallButton() backgroundExecutor.runAllReady() @@ -112,7 +112,7 @@ class EmergencyButtonControllerTest : SysuiTestCase() { /* isInCall= */ any(), /* hasTelephonyRadio= */ any(), /* simLocked= */ any(), - /* isSecure= */ any() + /* isSecure= */ any(), ) mainExecutor.runAllReady() verify(emergencyButton) @@ -120,7 +120,7 @@ class EmergencyButtonControllerTest : SysuiTestCase() { /* isInCall= */ eq(true), /* hasTelephonyRadio= */ eq(true), /* simLocked= */ any(), - /* isSecure= */ eq(true) + /* isSecure= */ eq(true), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt index 052d520ac92f..18b68d2fa8a3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt @@ -153,10 +153,12 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { private class FakeViewTransitionRegistry : IViewTransitionRegistry { val registry = mutableMapOf<ViewTransitionToken, View>() + val token = ViewTransitionToken() - override fun register(token: ViewTransitionToken, view: View) { + override fun register(view: View): ViewTransitionToken { registry[token] = view view.setTag(R.id.tag_view_transition_token, token) + return token } override fun unregister(token: ViewTransitionToken) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewTransitionRegistryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewTransitionRegistryTest.kt index ef91c793a2f3..b18eafd206ca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewTransitionRegistryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewTransitionRegistryTest.kt @@ -25,9 +25,9 @@ import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.runner.RunWith import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import kotlin.test.Test @SmallTest @@ -36,24 +36,22 @@ class ViewTransitionRegistryTest : SysuiTestCase() { private lateinit var view: View private lateinit var underTest: ViewTransitionRegistry - private var token: ViewTransitionToken = ViewTransitionToken() @Before fun setup() { view = FrameLayout(mContext) underTest = ViewTransitionRegistry() - token = ViewTransitionToken() } @Test fun testSuccessfulRegisterInViewTransitionRegistry() { - underTest.register(token, view) + val token = underTest.register(view) assertThat(underTest.getView(token)).isNotNull() } @Test fun testSuccessfulUnregisterInViewTransitionRegistry() { - underTest.register(token, view) + val token = underTest.register(view) assertThat(underTest.getView(token)).isNotNull() underTest.unregister(token) @@ -62,13 +60,14 @@ class ViewTransitionRegistryTest : SysuiTestCase() { @Test fun testSuccessfulUnregisterOnViewDetachedFromWindow() { - val view: View = mock { - on { getTag(R.id.tag_view_transition_token) } doReturn token - } + val view: View = mock() - underTest.register(token, view) + val token = underTest.register(view) + assertThat(token).isEqualTo(token) assertThat(underTest.getView(token)).isNotNull() + whenever(view.getTag(R.id.tag_view_transition_token)).thenReturn(token) + argumentCaptor<View.OnAttachStateChangeListener>() .apply { verify(view).addOnAttachStateChangeListener(capture()) } .firstValue @@ -76,4 +75,58 @@ class ViewTransitionRegistryTest : SysuiTestCase() { assertThat(underTest.getView(token)).isNull() } + + @Test + fun testMultipleRegisterOnSameView() { + val token = underTest.register(view) + + // multiple register on same view should return same token + assertThat(underTest.register(view)).isEqualTo(token) + + // 1st unregister doesn't remove the token from registry as refCount = 2 + underTest.unregister(token) + assertThat(underTest.getView(token)).isNotNull() + + // 2nd unregister removes the token from registry + underTest.unregister(token) + assertThat(underTest.getView(token)).isNull() + } + + @Test + fun testMultipleRegisterOnSameViewRemovedAfterViewDetached() { + val view: View = mock() + + val token = underTest.register(view) + whenever(view.getTag(R.id.tag_view_transition_token)).thenReturn(token) + + assertThat(underTest.getViewToken(view)).isEqualTo(token) + + // mock view's detach event + val caller = argumentCaptor<View.OnAttachStateChangeListener>() + .apply { verify(view).addOnAttachStateChangeListener(capture()) } + .firstValue + + // register 3 times + underTest.register(view) + underTest.register(view) + underTest.register(view) + + // unregister 1 time and verify entry should still be present in registry + underTest.unregister(token) + assertThat(underTest.getView(token)).isNotNull() + + // view's associated entry should be gone from registry, after view detaches + caller.onViewDetachedFromWindow(view) + assertThat(underTest.getView(token)).isNull() + } + + @Test + fun testDistinctViewsSameClassRegisterWithDifferentToken() { + var prev: ViewTransitionToken? = underTest.register(FrameLayout(mContext)) + for (i in 0 until 10) { + val curr = underTest.register(FrameLayout(mContext)) + assertThat(curr).isNotEqualTo(prev) + prev = curr + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 50762edc1179..88c9e74551fd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -21,6 +21,7 @@ import android.content.res.Configuration import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricManager +import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView @@ -290,7 +291,7 @@ open class AuthContainerViewTest : SysuiTestCase() { verify(callback) .onDismissed( - eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED), + eq(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED), eq<ByteArray?>(null), /* credentialAttestation */ eq(authContainer?.requestId ?: 0L), ) @@ -310,7 +311,7 @@ open class AuthContainerViewTest : SysuiTestCase() { ) verify(callback) .onDismissed( - eq(AuthDialogCallback.DISMISSED_USER_CANCELED), + eq(BiometricPrompt.DISMISSED_REASON_USER_CANCEL), eq<ByteArray?>(null), /* credentialAttestation */ eq(authContainer?.requestId ?: 0L), ) @@ -325,7 +326,7 @@ open class AuthContainerViewTest : SysuiTestCase() { verify(callback) .onDismissed( - eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE), + eq(BiometricPrompt.DISMISSED_REASON_NEGATIVE), eq<ByteArray?>(null), /* credentialAttestation */ eq(authContainer?.requestId ?: 0L), ) @@ -352,7 +353,7 @@ open class AuthContainerViewTest : SysuiTestCase() { verify(callback) .onDismissed( - eq(AuthDialogCallback.DISMISSED_ERROR), + eq(BiometricPrompt.DISMISSED_REASON_ERROR), eq<ByteArray?>(null), /* credentialAttestation */ eq(authContainer?.requestId ?: 0L), ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java index acc97a9f8642..a1a2aa70d869 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -138,9 +138,9 @@ public class AuthControllerTest extends SysuiTestCase { @Mock private IBiometricContextListener mContextListener; @Mock - private AuthDialog mDialog1; + private AuthContainerView mDialog1; @Mock - private AuthDialog mDialog2; + private AuthContainerView mDialog2; @Mock private CommandQueue mCommandQueue; @Mock @@ -382,7 +382,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception { showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -393,7 +393,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonNegative_whenDismissedByButtonNegative() throws Exception { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -404,7 +404,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonConfirmed_whenDismissedByButtonPositive() throws Exception { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -415,7 +415,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonConfirmNotRequired_whenDismissedByAuthenticated() throws Exception { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -426,7 +426,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonError_whenDismissedByError() throws Exception { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_ERROR, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_ERROR, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -437,7 +437,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonServerRequested_whenDismissedByServer() throws Exception { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -452,7 +452,7 @@ public class AuthControllerTest extends SysuiTestCase { final byte[] credentialAttestation = generateRandomHAT(); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED, credentialAttestation, mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( eq(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED), @@ -462,7 +462,7 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testSendsReasonContentViewMoreOptions_whenButtonPressed() throws Exception { showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS, null, /* credentialAttestation */ mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( @@ -696,7 +696,7 @@ public class AuthControllerTest extends SysuiTestCase { final byte[] credentialAttestation = generateRandomHAT(); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED, credentialAttestation, mAuthController.mCurrentDialog.getRequestId()); verify(mReceiver).onDialogDismissed( eq(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED), @@ -755,7 +755,7 @@ public class AuthControllerTest extends SysuiTestCase { public void testDoesNotCrash_whenTryAgainPressedAfterDismissal() { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); final long requestID = mAuthController.mCurrentDialog.getRequestId(); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null, /* credentialAttestation */requestID); mAuthController.onTryAgainPressed(requestID); } @@ -764,7 +764,7 @@ public class AuthControllerTest extends SysuiTestCase { public void testDoesNotCrash_whenDeviceCredentialPressedAfterDismissal() { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); final long requestID = mAuthController.mCurrentDialog.getRequestId(); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */, requestID); mAuthController.onDeviceCredentialPressed(requestID); } @@ -818,7 +818,7 @@ public class AuthControllerTest extends SysuiTestCase { // WHEN dialog is shown and then dismissed showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, + mAuthController.onDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */, mAuthController.mCurrentDialog.getRequestId()); @@ -1218,14 +1218,14 @@ public class AuthControllerTest extends SysuiTestCase { } @Override - protected AuthDialog buildDialog(DelayableExecutor bgExecutor, PromptInfo promptInfo, + protected AuthContainerView buildDialog(DelayableExecutor bgExecutor, PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds, String opPackageName, boolean skipIntro, long operationId, long requestId, WakefulnessLifecycle wakefulnessLifecycle, UserManager userManager, LockPatternUtils lockPatternUtils, PromptViewModel viewModel) { - AuthDialog dialog; + AuthContainerView dialog; if (mBuildCount == 0) { dialog = mDialog1; } else if (mBuildCount == 1) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java index 197cb843ba5f..9c3ef0426ee0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; @@ -39,6 +40,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor; import com.android.systemui.statusbar.phone.SystemUIDialog; import org.junit.Before; @@ -65,22 +67,25 @@ public class BiometricNotificationDialogFactoryTest extends SysuiTestCase { @Mock SystemUIDialog.Factory mSystemUIDialogFactory; @Mock SystemUIDialog mDialog; @Mock BiometricNotificationDialogFactory.ActivityStarter mActivityStarter; - private final ArgumentCaptor<DialogInterface.OnClickListener> mOnClickListenerArgumentCaptor = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); private final ArgumentCaptor<Intent> mIntentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); private BiometricNotificationDialogFactory mDialogFactory; + private FakeShadeDialogContextInteractor mDialogContextInteractor; @Before public void setUp() throws ExecutionException, InterruptedException { when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); - when(mSystemUIDialogFactory.create()).thenReturn(mDialog); + when(mSystemUIDialogFactory.create(any(Context.class))).thenReturn(mDialog); + + mDialogContextInteractor = new FakeShadeDialogContextInteractor(mContext); mDialogFactory = new BiometricNotificationDialogFactory( mResources, mSystemUIDialogFactory, + mDialogContextInteractor, mFingerprintManager, mFaceManager ); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index 98486a22854a..af6c65ec6d6d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -538,7 +538,6 @@ object TestShortcuts { simpleShortcutCategory(System, "System apps", "Open settings"), simpleShortcutCategory(System, "System controls", "Lock screen"), simpleShortcutCategory(System, "System controls", "View notifications"), - simpleShortcutCategory(System, "System apps", "Take a note"), simpleShortcutCategory(System, "System controls", "Take screenshot"), simpleShortcutCategory(System, "System controls", "Go back"), simpleShortcutCategory(MultiTasking, "Split screen", "Use full screen"), @@ -570,7 +569,6 @@ object TestShortcuts { simpleInputGestureData( keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL ), - simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES), simpleInputGestureData( keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT ), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt index 208abf39611d..6c4325adced4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt @@ -35,12 +35,20 @@ package com.android.systemui.keyguard.domain.interactor import android.os.PowerManager import android.platform.test.annotations.DisableFlags 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.systemui.Flags +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake +import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2Available +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -54,6 +62,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest @@ -62,6 +72,8 @@ import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings +import com.google.common.truth.Truth import junit.framework.Assert.assertEquals import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy @@ -416,4 +428,25 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { assertThat(transitionRepository) .startedTransition(from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN) } + + @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun testTransitionToGlanceableHub_onWakeUpFromAod() = + kosmos.runTest { + val user = setCommunalV2Available(true) + fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1, user.id) + batteryRepository.fake.setDevicePluggedIn(true) + + val currentScene by collectLastValue(communalSceneInteractor.currentScene) + fakeCommunalSceneRepository.changeScene(CommunalScenes.Blank) + + // Communal is not showing + Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Blank) + + powerInteractor.setAwakeForTest() + testScope.advanceTimeBy(100) // account for debouncing + + Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Communal) + assertThat(transitionRepository).noTransitionsStarted() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt new file mode 100644 index 000000000000..052dfd52887f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2025 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.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DozingToDreamingTransitionViewModelTest : SysuiTestCase() { + val kosmos = testKosmos() + + val underTest by lazy { kosmos.dozingToDreamingTransitionViewModel } + + @Test + fun notificationShadeAlpha() = + kosmos.runTest { + val values by collectValues(underTest.notificationAlpha) + assertThat(values).isEmpty() + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.DOZING, + to = KeyguardState.DREAMING, + testScope, + ) + + assertThat(values).isNotEmpty() + values.forEach { assertThat(it).isEqualTo(0) } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt index 4e14fec8408f..943ada9346e7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt @@ -18,6 +18,9 @@ package com.android.systemui.media.controls.ui.binder import android.animation.Animator import android.animation.ObjectAnimator +import android.icu.text.MeasureFormat +import android.icu.util.Measure +import android.icu.util.MeasureUnit import android.testing.TestableLooper import android.view.View import android.widget.SeekBar @@ -30,6 +33,7 @@ import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat +import java.util.Locale import org.junit.Before import org.junit.Rule import org.junit.Test @@ -61,11 +65,11 @@ class SeekBarObserverTest : SysuiTestCase() { fun setUp() { context.orCreateTestableResources.addOverride( R.dimen.qs_media_enabled_seekbar_height, - enabledHeight + enabledHeight, ) context.orCreateTestableResources.addOverride( R.dimen.qs_media_disabled_seekbar_height, - disabledHeight + disabledHeight, ) seekBarView = SeekBar(context) @@ -110,14 +114,31 @@ class SeekBarObserverTest : SysuiTestCase() { @Test fun seekBarProgress() { + val elapsedTime = 3000 + val duration = (1.5 * 60 * 60 * 1000).toInt() // WHEN part of the track has been played - val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000, true) + val data = SeekBarViewModel.Progress(true, true, true, false, elapsedTime, duration, true) observer.onChanged(data) // THEN seek bar shows the progress - assertThat(seekBarView.progress).isEqualTo(3000) - assertThat(seekBarView.max).isEqualTo(120000) - - val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00") + assertThat(seekBarView.progress).isEqualTo(elapsedTime) + assertThat(seekBarView.max).isEqualTo(duration) + + val expectedProgress = + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures(Measure(3, MeasureUnit.SECOND)) + val expectedDuration = + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures( + Measure(1, MeasureUnit.HOUR), + Measure(30, MeasureUnit.MINUTE), + Measure(0, MeasureUnit.SECOND), + ) + val desc = + context.getString( + R.string.controls_media_seekbar_description, + expectedProgress, + expectedDuration, + ) assertThat(seekBarView.contentDescription).isEqualTo(desc) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt index 645efae16b8b..ab217a3f50ef 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt @@ -31,21 +31,18 @@ class MutableSelectionStateTest : SysuiTestCase() { @Test fun selectTile_isCorrectlySelected() { - assertThat(underTest.selection?.tileSpec).isNotEqualTo(TEST_SPEC) + assertThat(underTest.selection).isNotEqualTo(TEST_SPEC) - underTest.select(TEST_SPEC, manual = true) - assertThat(underTest.selection?.tileSpec).isEqualTo(TEST_SPEC) - assertThat(underTest.selection?.manual).isTrue() + underTest.select(TEST_SPEC) + assertThat(underTest.selection).isEqualTo(TEST_SPEC) underTest.unSelect() assertThat(underTest.selection).isNull() val newSpec = TileSpec.create("newSpec") - underTest.select(TEST_SPEC, manual = true) - underTest.select(newSpec, manual = false) - assertThat(underTest.selection?.tileSpec).isNotEqualTo(TEST_SPEC) - assertThat(underTest.selection?.tileSpec).isEqualTo(newSpec) - assertThat(underTest.selection?.manual).isFalse() + underTest.select(TEST_SPEC) + underTest.select(newSpec) + assertThat(underTest.selection).isEqualTo(newSpec) } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java index fee358a7c15d..83860ecf168b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java @@ -63,6 +63,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.List; +import java.util.Optional; import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; @@ -100,7 +101,7 @@ public class RotationLockTileTest extends SysuiTestCase { @Mock private BatteryController mBatteryController; @Mock - DeviceStateRotationLockSettingController mDeviceStateRotationLockSettingController; + Optional<DeviceStateRotationLockSettingController> mDeviceStateRotationLockSettingController; @Mock RotationPolicyWrapper mRotationPolicyWrapper; @Mock diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt index 97a10e68960f..e5191e92c6d8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt @@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R import com.android.systemui.SysuiTestCase +import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -46,24 +47,27 @@ class DataSaverDialogDelegateTest : SysuiTestCase() { private lateinit var sysuiDialogFactory: SystemUIDialog.Factory private lateinit var sysuiDialog: SystemUIDialog private lateinit var dataSaverDialogDelegate: DataSaverDialogDelegate + private lateinit var contextInteractor: FakeShadeDialogContextInteractor @Before fun setup() { sysuiDialog = mock<SystemUIDialog>() sysuiDialogFactory = mock<SystemUIDialog.Factory>() + contextInteractor = FakeShadeDialogContextInteractor(context) dataSaverDialogDelegate = DataSaverDialogDelegate( sysuiDialogFactory, - context, + contextInteractor, EmptyCoroutineContext, dataSaverController, - mock<SharedPreferences>() + mock<SharedPreferences>(), ) whenever(sysuiDialogFactory.create(eq(dataSaverDialogDelegate), eq(context))) .thenReturn(sysuiDialog) } + @Test fun delegateSetsDialogTitleCorrectly() { val expectedResId = R.string.data_saver_enable_title diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractorTest.kt index 2e9f24c67cad..87ac034a6316 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractorTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.qs.tiles.base.actions.intentInputs import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.saver.domain.model.DataSaverTileModel import com.android.systemui.settings.UserFileManager +import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -53,17 +54,19 @@ class DataSaverTileUserActionInteractorTest : SysuiTestCase() { private lateinit var sharedPreferences: SharedPreferences private lateinit var dialogFactory: SystemUIDialog.Factory private lateinit var underTest: DataSaverTileUserActionInteractor + private lateinit var contextInteractor: FakeShadeDialogContextInteractor @Before fun setup() { userFileManager = mock<UserFileManager>() sharedPreferences = mock<SharedPreferences>() dialogFactory = mock<SystemUIDialog.Factory>() + contextInteractor = FakeShadeDialogContextInteractor(mContext) whenever( userFileManager.getSharedPreferences( eq(DataSaverTileUserActionInteractor.PREFS), eq(Context.MODE_PRIVATE), - eq(context.userId) + eq(context.userId), ) ) .thenReturn(sharedPreferences) @@ -71,6 +74,7 @@ class DataSaverTileUserActionInteractorTest : SysuiTestCase() { underTest = DataSaverTileUserActionInteractor( context, + contextInteractor, EmptyCoroutineContext, EmptyCoroutineContext, dataSaverController, @@ -87,7 +91,7 @@ class DataSaverTileUserActionInteractorTest : SysuiTestCase() { whenever( sharedPreferences.getBoolean( eq(DataSaverTileUserActionInteractor.DIALOG_SHOWN), - any() + any(), ) ) .thenReturn(true) @@ -107,7 +111,7 @@ class DataSaverTileUserActionInteractorTest : SysuiTestCase() { whenever( sharedPreferences.getBoolean( eq(DataSaverTileUserActionInteractor.DIALOG_SHOWN), - any() + any(), ) ) .thenReturn(false) @@ -128,7 +132,7 @@ class DataSaverTileUserActionInteractorTest : SysuiTestCase() { whenever( sharedPreferences.getBoolean( eq(DataSaverTileUserActionInteractor.DIALOG_SHOWN), - any() + any(), ) ) .thenReturn(false) @@ -144,7 +148,7 @@ class DataSaverTileUserActionInteractorTest : SysuiTestCase() { whenever( sharedPreferences.getBoolean( eq(DataSaverTileUserActionInteractor.DIALOG_SHOWN), - any() + any(), ) ) .thenReturn(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt index 6e26fa119888..701e55d0759d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt @@ -24,17 +24,22 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.shadeModeInteractor 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 @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @@ -79,4 +84,22 @@ class QuickSettingsContainerViewModelTest : SysuiTestCase() { assertThat(underTest.showHeader).isFalse() } + + @Test + fun showMedia_activeMedia_true() = + testScope.runTest { + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true)) + runCurrent() + + assertThat(underTest.showMedia).isTrue() + } + + @Test + fun showMedia_noActiveMedia_false() = + testScope.runTest { + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = false)) + runCurrent() + + assertThat(underTest.showMedia).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt index 84fc93008f49..a3dd67f85150 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt @@ -22,6 +22,9 @@ import android.provider.AlarmClock import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback @@ -31,22 +34,32 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argThat import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import java.util.Date +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatcher -import org.mockito.Mockito.times import org.mockito.Mockito.verify +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class ShadeHeaderClockInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() private val testScope = kosmos.testScope private val activityStarter = kosmos.activityStarter private val nextAlarmController = kosmos.nextAlarmController - val underTest = kosmos.shadeHeaderClockInteractor + private val underTest = kosmos.shadeHeaderClockInteractor @Test fun launchClockActivity_default() = @@ -55,7 +68,7 @@ class ShadeHeaderClockInteractorTest : SysuiTestCase() { verify(activityStarter) .postStartActivityDismissingKeyguard( argThat(IntentMatcherAction(AlarmClock.ACTION_SHOW_ALARMS)), - any() + any(), ) } @@ -71,6 +84,75 @@ class ShadeHeaderClockInteractorTest : SysuiTestCase() { underTest.launchClockActivity() verify(activityStarter).postStartActivityDismissingKeyguard(any()) } + + @Test + fun onTimezoneOrLocaleChanged_localeAndTimezoneChanged_emitsForEach() = + testScope.runTest { + val timeZoneOrLocaleChanges by collectValues(underTest.onTimezoneOrLocaleChanged) + + sendIntentActionBroadcast(Intent.ACTION_TIMEZONE_CHANGED) + sendIntentActionBroadcast(Intent.ACTION_LOCALE_CHANGED) + sendIntentActionBroadcast(Intent.ACTION_LOCALE_CHANGED) + sendIntentActionBroadcast(Intent.ACTION_TIMEZONE_CHANGED) + + assertThat(timeZoneOrLocaleChanges).hasSize(4) + } + + @Test + fun onTimezoneOrLocaleChanged_timeChanged_doesNotEmit() = + testScope.runTest { + val timeZoneOrLocaleChanges by collectValues(underTest.onTimezoneOrLocaleChanged) + assertThat(timeZoneOrLocaleChanges).hasSize(1) + + sendIntentActionBroadcast(Intent.ACTION_TIME_CHANGED) + sendIntentActionBroadcast(Intent.ACTION_TIME_TICK) + + // Expect only 1 event to have been emitted onStart, but no more. + assertThat(timeZoneOrLocaleChanges).hasSize(1) + } + + @Test + fun currentTime_timeChanged() = + testScope.runTest { + val currentTime by collectLastValue(underTest.currentTime) + + sendIntentActionBroadcast(Intent.ACTION_TIME_CHANGED) + val earlierTime = checkNotNull(currentTime) + + advanceTimeBy(3.seconds) + runCurrent() + + sendIntentActionBroadcast(Intent.ACTION_TIME_CHANGED) + val laterTime = checkNotNull(currentTime) + + assertThat(differenceBetween(laterTime, earlierTime)).isEqualTo(3.seconds) + } + + @Test + fun currentTime_timeTicked() = + testScope.runTest { + val currentTime by collectLastValue(underTest.currentTime) + + sendIntentActionBroadcast(Intent.ACTION_TIME_TICK) + val earlierTime = checkNotNull(currentTime) + + advanceTimeBy(7.seconds) + runCurrent() + + sendIntentActionBroadcast(Intent.ACTION_TIME_TICK) + val laterTime = checkNotNull(currentTime) + + assertThat(differenceBetween(laterTime, earlierTime)).isEqualTo(7.seconds) + } + + private fun differenceBetween(date1: Date, date2: Date): Duration { + return (date1.time - date2.time).milliseconds + } + + private fun TestScope.sendIntentActionBroadcast(intentAction: String) { + kosmos.broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, Intent(intentAction)) + runCurrent() + } } private class IntentMatcherAction(private val action: String) : ArgumentMatcher<Intent> { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index 061e04ef29f7..37b4688f753d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -25,12 +25,15 @@ import com.android.systemui.shade.domain.interactor.disableDualShade import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.enableSingleShade import com.android.systemui.shade.domain.interactor.enableSplitShade +import com.android.systemui.shade.domain.interactor.shadeMode +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argThat import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -43,6 +46,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @EnableSceneContainer @@ -64,14 +68,15 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun mobileSubIds_update() = testScope.runTest { - val mobileSubIds by collectLastValue(underTest.mobileSubIds) mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) + runCurrent() - assertThat(mobileSubIds).isEqualTo(listOf(1)) + assertThat(underTest.mobileSubIds).isEqualTo(listOf(1)) mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + runCurrent() - assertThat(mobileSubIds).isEqualTo(listOf(1, 2)) + assertThat(underTest.mobileSubIds).isEqualTo(listOf(1, 2)) } @Test @@ -116,13 +121,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onSystemIconChipClicked_lockedOnQsShade_collapsesShadeToLockscreen() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(false) - setScene(Scenes.Lockscreen) - setOverlay(Overlays.QuickSettingsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onSystemIconChipClicked() runCurrent() @@ -134,13 +135,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onSystemIconChipClicked_lockedOnNotifShade_expandsQsShade() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(false) - setScene(Scenes.Lockscreen) - setOverlay(Overlays.NotificationsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onSystemIconChipClicked() runCurrent() @@ -166,13 +163,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onSystemIconChipClicked_unlockedOnQsShade_collapsesShadeToGone() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.QuickSettingsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.QuickSettingsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onSystemIconChipClicked() runCurrent() @@ -184,13 +177,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onSystemIconChipClicked_unlockedOnNotifShade_expandsQsShade() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.NotificationsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.NotificationsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onSystemIconChipClicked() runCurrent() @@ -203,13 +192,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onNotificationIconChipClicked_lockedOnNotifShade_collapsesShadeToLockscreen() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(false) - setScene(Scenes.Lockscreen) - setOverlay(Overlays.NotificationsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onNotificationIconChipClicked() runCurrent() @@ -221,13 +206,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onNotificationIconChipClicked_lockedOnQsShade_expandsNotifShade() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(false) - setScene(Scenes.Lockscreen) - setOverlay(Overlays.QuickSettingsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onNotificationIconChipClicked() runCurrent() @@ -240,13 +221,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onNotificationIconChipClicked_unlockedOnNotifShade_collapsesShadeToGone() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.NotificationsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.NotificationsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onNotificationIconChipClicked() runCurrent() @@ -258,13 +235,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onNotificationIconChipClicked_unlockedOnQsShade_expandsNotifShade() = testScope.runTest { - kosmos.enableDualShade() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.QuickSettingsShade) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.QuickSettingsShade) - assertThat(currentOverlays).isNotEmpty() underTest.onNotificationIconChipClicked() runCurrent() @@ -319,22 +292,13 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun highlightChips_notifsOpenInDualShade_notifsStrongQuickSettingsWeak() = testScope.runTest { - kosmos.enableDualShade() - val currentScene by collectLastValue(sceneInteractor.currentScene) - val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - // Test the lockscreen scenario. - setScene(Scenes.Lockscreen) - setOverlay(Overlays.NotificationsShade) + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) // Test the unlocked scenario. - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.NotificationsShade) - assertThat(currentScene).isEqualTo(Scenes.Gone) - assertThat(currentOverlays).isNotEmpty() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.NotificationsShade) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) } @@ -342,22 +306,13 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun highlightChips_quickSettingsOpenInDualShade_notifsWeakQuickSettingsStrong() = testScope.runTest { - kosmos.enableDualShade() - val currentScene by collectLastValue(sceneInteractor.currentScene) - val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - // Test the lockscreen scenario. - setScene(Scenes.Lockscreen) - setOverlay(Overlays.QuickSettingsShade) + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) // Test the unlocked scenario. - setDeviceEntered(true) - setScene(Scenes.Gone) - setOverlay(Overlays.QuickSettingsShade) - assertThat(currentScene).isEqualTo(Scenes.Gone) - assertThat(currentOverlays).isNotEmpty() + setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.QuickSettingsShade) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong) } @@ -365,21 +320,13 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun highlightChips_noOverlaysInDualShade_bothNone() = testScope.runTest { - kosmos.enableDualShade() - val currentScene by collectLastValue(sceneInteractor.currentScene) - val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - // Test the lockscreen scenario. - setScene(Scenes.Lockscreen) - assertThat(currentOverlays).isEmpty() + setupDualShadeState(scene = Scenes.Lockscreen) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) // Test the unlocked scenario. - setDeviceEntered(true) - setScene(Scenes.Gone) - assertThat(currentScene).isEqualTo(Scenes.Gone) - assertThat(currentOverlays).isEmpty() + setupDualShadeState(scene = Scenes.Gone) assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None) assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None) } @@ -401,21 +348,43 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { ) } - private fun setScene(key: SceneKey) { - sceneInteractor.changeScene(key, "test") + private fun TestScope.setupDualShadeState(scene: SceneKey, overlay: OverlayKey? = null) { + kosmos.enableDualShade() + val shadeMode by collectLastValue(kosmos.shadeMode) + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + if (scene == Scenes.Gone) { + // Unlock the device, marking the device has been entered. + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + } + runCurrent() + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + + sceneInteractor.changeScene(scene, "test") + checkNotNull(currentOverlays).forEach { sceneInteractor.instantlyHideOverlay(it, "test") } + runCurrent() + overlay?.let { sceneInteractor.showOverlay(it, "test") } sceneInteractor.setTransitionState( - MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(scene, setOfNotNull(overlay)) + ) ) - testScope.runCurrent() + runCurrent() + + assertThat(currentScene).isEqualTo(scene) + if (overlay == null) { + assertThat(currentOverlays).isEmpty() + } else { + assertThat(currentOverlays).containsExactly(overlay) + } } - private fun setOverlay(key: OverlayKey) { - val currentOverlays = sceneInteractor.currentOverlays.value + key - sceneInteractor.showOverlay(key, "test") + private fun setScene(key: SceneKey) { + sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(sceneInteractor.currentScene.value, currentOverlays) - ) + MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) ) testScope.runCurrent() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java index c6801f1ad9d5..3d8da6140ff7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -23,8 +23,8 @@ import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_VIA_SYS import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -35,14 +35,12 @@ import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback; import android.os.Bundle; -import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.view.KeyEvent; import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.Appearance; import android.view.WindowInsetsController.Behavior; -import android.view.accessibility.Flags; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -385,30 +383,7 @@ public class CommandQueueTest extends SysuiTestCase { } @Test - @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) - public void addQsTile_withA11yQsShortcutFlagOff() { - ComponentName c = new ComponentName("testpkg", "testcls"); - - mCommandQueue.addQsTile(c); - waitForIdleSync(); - - verify(mCallbacks).addQsTile(eq(c)); - } - - @Test - @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) - public void addQsTileToFrontOrEnd_withA11yQsShortcutFlagOff_doNothing() { - ComponentName c = new ComponentName("testpkg", "testcls"); - - mCommandQueue.addQsTileToFrontOrEnd(c, true); - waitForIdleSync(); - - verifyNoMoreInteractions(mCallbacks); - } - - @Test - @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) - public void addQsTile_withA11yQsShortcutFlagOn() { + public void addQsTile() { ComponentName c = new ComponentName("testpkg", "testcls"); mCommandQueue.addQsTile(c); @@ -418,8 +393,7 @@ public class CommandQueueTest extends SysuiTestCase { } @Test - @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) - public void addQsTileAtTheEnd_withA11yQsShortcutFlagOn() { + public void addQsTileAtTheEnd() { ComponentName c = new ComponentName("testpkg", "testcls"); mCommandQueue.addQsTileToFrontOrEnd(c, true); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt index e38ea30daf0e..9fd189f35c32 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt @@ -20,11 +20,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest @@ -33,22 +36,20 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class CallChipInteractorTest : SysuiTestCase() { - val kosmos = Kosmos() + val kosmos = testKosmos().useUnconfinedTestDispatcher() val repo = kosmos.ongoingCallRepository val underTest = kosmos.callChipInteractor @Test - fun ongoingCallState_matchesRepo() = - kosmos.testScope.runTest { + fun ongoingCallState_matchesState() = + kosmos.runTest { val latest by collectLastValue(underTest.ongoingCallState) - val inCall = inCallModel(startTimeMs = 1000) - repo.setOngoingCallState(inCall) - assertThat(latest).isEqualTo(inCall) + addOngoingCallState(key = "testKey") + assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) - val noCall = OngoingCallModel.NoCall - repo.setOngoingCallState(noCall) - assertThat(latest).isEqualTo(noCall) + removeOngoingCallState(key = "testKey") + assertThat(latest).isEqualTo(OngoingCallModel.NoCall) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index c7b3175a636f..fda4ab005446 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -27,7 +27,9 @@ import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.plugins.activityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView @@ -35,17 +37,11 @@ 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.core.StatusBarConnectedDisplays -import com.android.systemui.statusbar.core.StatusBarRootModernization -import com.android.systemui.statusbar.notification.data.model.activeNotificationModel -import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository -import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore -import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel -import com.android.systemui.statusbar.notification.shared.CallType -import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization -import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel +import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat @@ -60,10 +56,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class CallChipViewModelTest : SysuiTestCase() { - private val kosmos = testKosmos() - private val notificationListRepository = kosmos.activeNotificationListRepository - private val testScope = kosmos.testScope - private val repo = kosmos.ongoingCallRepository + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val chipBackgroundView = mock<ChipBackgroundContainer>() private val chipView = @@ -82,53 +75,53 @@ class CallChipViewModelTest : SysuiTestCase() { @Test fun chip_noCall_isHidden() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState("testKey") assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java) } @Test fun chip_inCall_zeroStartTime_isShownAsIconOnly() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState(inCallModel(startTimeMs = 0)) + addOngoingCallState(startTimeMs = 0) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } @Test fun chip_inCall_negativeStartTime_isShownAsIconOnly() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState(inCallModel(startTimeMs = -2)) + addOngoingCallState(startTimeMs = -2) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } @Test fun chip_inCall_positiveStartTime_isShownAsTimer() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState(inCallModel(startTimeMs = 345)) + addOngoingCallState(startTimeMs = 345) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } @Test fun chip_inCall_startTimeConvertedToElapsedRealtime() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) kosmos.fakeSystemClock.setCurrentTimeMillis(3000) kosmos.fakeSystemClock.setElapsedRealtime(400_000) - repo.setOngoingCallState(inCallModel(startTimeMs = 1000)) + addOngoingCallState(startTimeMs = 1000) // The OngoingCallModel start time is relative to currentTimeMillis, so this call // started 2000ms ago (1000 - 3000). The OngoingActivityChipModel start time needs to be @@ -141,13 +134,11 @@ class CallChipViewModelTest : SysuiTestCase() { @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_positiveStartTime_connectedDisplaysFlagOn_iconIsNotifIcon() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) val notifKey = "testNotifKey" - repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, notificationIcon = null, notificationKey = notifKey) - ) + addOngoingCallState(startTimeMs = 1000, statusBarChipIconView = null, key = notifKey) assertThat((latest as OngoingActivityChipModel.Active).icon) .isInstanceOf( @@ -163,16 +154,14 @@ class CallChipViewModelTest : SysuiTestCase() { @Test @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_zeroStartTime_cdFlagOff_iconIsNotifIcon_withContentDescription() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) val notifIcon = createStatusBarIconViewOrNull() - repo.setOngoingCallState( - inCallModel( - startTimeMs = 0, - notificationIcon = notifIcon, - appName = "Fake app name", - ) + addOngoingCallState( + startTimeMs = 0, + statusBarChipIconView = notifIcon, + appName = "Fake app name", ) assertThat((latest as OngoingActivityChipModel.Active).icon) @@ -190,16 +179,13 @@ class CallChipViewModelTest : SysuiTestCase() { @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_zeroStartTime_cdFlagOn_iconIsNotifKeyIcon_withContentDescription() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState( - inCallModel( - startTimeMs = 0, - notificationIcon = createStatusBarIconViewOrNull(), - notificationKey = "notifKey", - appName = "Fake app name", - ) + addOngoingCallState( + key = "notifKey", + statusBarChipIconView = createStatusBarIconViewOrNull(), + appName = "Fake app name", ) assertThat((latest as OngoingActivityChipModel.Active).icon) @@ -219,10 +205,10 @@ class CallChipViewModelTest : SysuiTestCase() { @Test @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOff_iconIsPhone() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = null)) + addOngoingCallState(statusBarChipIconView = null) assertThat((latest as OngoingActivityChipModel.Active).icon) .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) @@ -237,16 +223,13 @@ class CallChipViewModelTest : SysuiTestCase() { @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOn_iconIsNotifKeyIcon_withContentDescription() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState( - inCallModel( - startTimeMs = 1000, - notificationIcon = null, - notificationKey = "notifKey", - appName = "Fake app name", - ) + addOngoingCallState( + key = "notifKey", + statusBarChipIconView = null, + appName = "Fake app name", ) assertThat((latest as OngoingActivityChipModel.Active).icon) @@ -265,10 +248,10 @@ class CallChipViewModelTest : SysuiTestCase() { @Test fun chip_positiveStartTime_colorsAreAccentThemed() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState(inCallModel(startTimeMs = 1000, promotedContent = null)) + addOngoingCallState(startTimeMs = 1000, promotedContent = null) assertThat((latest as OngoingActivityChipModel.Active).colors) .isEqualTo(ColorsModel.AccentThemed) @@ -276,10 +259,10 @@ class CallChipViewModelTest : SysuiTestCase() { @Test fun chip_zeroStartTime_colorsAreAccentThemed() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState(inCallModel(startTimeMs = 0, promotedContent = null)) + addOngoingCallState(startTimeMs = 0, promotedContent = null) assertThat((latest as OngoingActivityChipModel.Active).colors) .isEqualTo(ColorsModel.AccentThemed) @@ -287,19 +270,19 @@ class CallChipViewModelTest : SysuiTestCase() { @Test fun chip_resetsCorrectly() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) kosmos.fakeSystemClock.setCurrentTimeMillis(3000) kosmos.fakeSystemClock.setElapsedRealtime(400_000) // Start a call - repo.setOngoingCallState(inCallModel(startTimeMs = 1000)) + addOngoingCallState(key = "testKey", startTimeMs = 1000) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java) assertThat((latest as OngoingActivityChipModel.Active.Timer).startTimeMs) .isEqualTo(398_000) // End the call - repo.setOngoingCallState(OngoingCallModel.NoCall) + removeOngoingCallState(key = "testKey") assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java) // Let 100_000ms elapse @@ -307,31 +290,31 @@ class CallChipViewModelTest : SysuiTestCase() { kosmos.fakeSystemClock.setElapsedRealtime(500_000) // Start a new call, which started 1000ms ago - repo.setOngoingCallState(inCallModel(startTimeMs = 102_000)) + addOngoingCallState(key = "testKey", startTimeMs = 102_000) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java) assertThat((latest as OngoingActivityChipModel.Active.Timer).startTimeMs) .isEqualTo(499_000) } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_inCall_nullIntent_nullClickListener() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState(inCallModel(startTimeMs = 1000, intent = null)) + addOngoingCallState(contentIntent = null) assertThat((latest as OngoingActivityChipModel.Active).onClickListenerLegacy).isNull() } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_inCall_positiveStartTime_validIntent_clickListenerLaunchesIntent() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) val pendingIntent = mock<PendingIntent>() - repo.setOngoingCallState(inCallModel(startTimeMs = 1000, intent = pendingIntent)) + addOngoingCallState(startTimeMs = 1000, contentIntent = pendingIntent) val clickListener = (latest as OngoingActivityChipModel.Active).onClickListenerLegacy assertThat(clickListener).isNotNull() @@ -343,13 +326,13 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_inCall_zeroStartTime_validIntent_clickListenerLaunchesIntent() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) val pendingIntent = mock<PendingIntent>() - repo.setOngoingCallState(inCallModel(startTimeMs = 0, intent = pendingIntent)) + addOngoingCallState(startTimeMs = 0, contentIntent = pendingIntent) val clickListener = (latest as OngoingActivityChipModel.Active).onClickListenerLegacy assertThat(clickListener).isNotNull() @@ -362,33 +345,25 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_inCall_nullIntent_noneClickBehavior() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) - postOngoingCallNotification( - repository = notificationListRepository, - startTimeMs = 1000L, - intent = null, - ) + addOngoingCallState(startTimeMs = 1000, contentIntent = null) assertThat((latest as OngoingActivityChipModel.Active).clickBehavior) .isInstanceOf(OngoingActivityChipModel.ClickBehavior.None::class.java) } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_inCall_positiveStartTime_validIntent_clickBehaviorLaunchesIntent() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) val pendingIntent = mock<PendingIntent>() - postOngoingCallNotification( - repository = notificationListRepository, - startTimeMs = 1000L, - intent = pendingIntent, - ) + addOngoingCallState(startTimeMs = 1000, contentIntent = pendingIntent) val clickBehavior = (latest as OngoingActivityChipModel.Active).clickBehavior assertThat(clickBehavior) @@ -403,17 +378,13 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_inCall_zeroStartTime_validIntent_clickBehaviorLaunchesIntent() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chip) val pendingIntent = mock<PendingIntent>() - postOngoingCallNotification( - repository = notificationListRepository, - startTimeMs = 0L, - intent = pendingIntent, - ) + addOngoingCallState(startTimeMs = 0, contentIntent = pendingIntent) val clickBehavior = (latest as OngoingActivityChipModel.Active).clickBehavior assertThat(clickBehavior) @@ -435,27 +406,6 @@ class CallChipViewModelTest : SysuiTestCase() { mock<StatusBarIconView>() } - fun postOngoingCallNotification( - repository: ActiveNotificationListRepository, - startTimeMs: Long, - intent: PendingIntent?, - ) { - repository.activeNotifications.value = - ActiveNotificationsStore.Builder() - .apply { - addIndividualNotif( - activeNotificationModel( - key = "notif1", - whenTime = startTimeMs, - callType = CallType.Ongoing, - statusBarChipIcon = null, - contentIntent = intent, - ) - ) - } - .build() - } - private val PROMOTED_CONTENT_WITH_COLOR = PromotedNotificationContentModel.Builder("notif") .apply { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt index cd3c8cdcd6e9..ccc844ad5837 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.content.DialogInterface -import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -48,10 +47,10 @@ 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.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog -import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory -import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.statusbar.policy.CastDevice import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat @@ -438,7 +437,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_projectionStateEntireScreen_clickListenerShowsScreenCastDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -454,7 +453,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_projectionStateSingleTask_clickListenerShowsScreenCastDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -475,7 +474,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_routerStateCasting_clickListenerShowsGenericCastDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -505,7 +504,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_projectionStateCasting_clickListenerHasCuj() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -525,7 +524,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_routerStateCasting_clickListenerHasCuj() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -554,7 +553,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_routerStateCasting_hasClickBehavior() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -575,7 +574,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_projectionStateCasting_hasClickBehavior() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -588,7 +587,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_projectionStateEntireScreen_clickBehaviorShowsScreenCastDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -605,7 +604,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_projectionStateSingleTask_clickBehaviorShowsScreenCastDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -627,7 +626,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_routerStateCasting_clickBehaviorShowsGenericCastDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index 05ca7aeaa664..4993b5661373 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -38,7 +38,6 @@ import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifCh import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.core.StatusBarConnectedDisplays -import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.UnconfinedFakeHeadsUpRowRepository @@ -48,7 +47,8 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository -import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat @@ -761,11 +761,8 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test - @DisableFlags( - FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY, - StatusBarRootModernization.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME, - ) + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + @DisableChipsModernization fun chips_chipsModernizationDisabled_clickingChipNotifiesInteractor() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -793,7 +790,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chips_chipsModernizationEnabled_clickingChipNotifiesInteractor() = kosmos.runTest { val latest by collectLastValue(underTest.chips) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index 6cfad8540491..005af366a6c0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt @@ -17,8 +17,6 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.content.DialogInterface -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -44,10 +42,10 @@ 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.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog -import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory -import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat @@ -282,7 +280,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_notProjecting_clickListenerShowsDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -299,7 +297,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_projectingEntireScreen_clickListenerShowsDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -317,7 +315,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_projectingSingleTask_clickListenerShowsDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -339,7 +337,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_clickListenerHasCujLegacy() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -359,7 +357,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_recordingState_hasClickBehavior() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -370,7 +368,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_notProjecting_expandActionBehaviorShowsDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -386,7 +384,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_projectingEntireScreen_expandActionBehaviorShowsDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -401,7 +399,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_projectingSingleTask_expandActionBehaviorShowsDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index e708382799c2..d6b10a89726e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt @@ -49,10 +49,10 @@ 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.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog -import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory -import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat @@ -506,7 +506,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { @Test @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP) - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_noScreen_clickListenerShowsGenericShareDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -527,7 +527,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_entireScreen_clickListenerShowsScreenShareDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -548,7 +548,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_singleTask_clickListenerShowsScreenShareDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -573,7 +573,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun chip_clickListenerHasCuj() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -597,7 +597,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_noScreen_hasClickBehavior() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -609,7 +609,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_entireScreen_hasClickBehavior() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -621,7 +621,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_singleTask_hasClickBehavior() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -637,11 +637,8 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags( - FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP, - StatusBarRootModernization.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME, - ) + @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP) + @EnableChipsModernization fun chip_noScreen_clickBehaviorShowsGenericShareDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -657,7 +654,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_entireScreen_clickBehaviorShowsScreenShareDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -673,7 +670,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun chip_singleTask_clickBehaviorShowsScreenShareDialog() = testScope.runTest { val latest by collectLastValue(underTest.chip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt index fc3af11c30b3..39b19d3c4191 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.chips.ui.viewmodel -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -31,9 +29,9 @@ 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.createDialogLaunchOnClickCallback import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener -import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import kotlin.test.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean @@ -64,7 +62,7 @@ class OngoingActivityChipViewModelTest : SysuiTestCase() { mock<Expandable>().apply { whenever(dialogTransitionController(any())).thenReturn(mock()) } @Test - @DisableFlags(StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization fun createDialogLaunchOnClickListener_showsDialogOnClick() { val cuj = DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Test") val clickListener = @@ -82,7 +80,7 @@ class OngoingActivityChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun createDialogLaunchOnClickCallback_showsDialogOnClick() { val cuj = DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Test") val clickCallback = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 20637cd4af33..2887de38fe23 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -22,7 +22,6 @@ import android.content.res.Configuration import android.content.res.mainResources import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable -import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -57,7 +56,6 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsVie import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog -import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -66,7 +64,8 @@ import com.android.systemui.statusbar.notification.data.repository.addNotifs import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState import com.android.systemui.testKosmos @@ -138,7 +137,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_allHidden_bothPrimaryAndSecondaryHidden() = kosmos.runTest { @@ -155,7 +154,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_allInactive() = kosmos.runTest { @@ -184,7 +183,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsScreenRecordChip(latest) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_screenRecordShow_restHidden_primaryIsScreenRecordSecondaryIsHidden() = kosmos.runTest { @@ -201,7 +200,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_screenRecordActive_restInactive() = kosmos.runTest { @@ -230,7 +229,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsScreenRecordChip(latest) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_screenRecordShowAndCallShow_primaryIsScreenRecordSecondaryIsCall() = kosmos.runTest { @@ -246,7 +245,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_screenRecordAndCallActive_inThatOrder() = kosmos.runTest { @@ -265,7 +264,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_oneChip_notSquished() = kosmos.runTest { @@ -278,7 +277,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_oneChip_notSquished() = kosmos.runTest { @@ -291,7 +290,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } - @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_twoTimerChips_isSmallPortrait_bothSquished() = kosmos.runTest { @@ -307,7 +306,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } - @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_twoTimerChips_isSmallPortrait_bothSquished() = kosmos.runTest { @@ -323,7 +322,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } - @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = kosmos.runTest { @@ -340,7 +339,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } - @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = kosmos.runTest { @@ -357,7 +356,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } - @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_numberOfChipsChanges_chipsGetSquishedAndUnsquished() = kosmos.runTest { @@ -393,7 +392,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Inactive::class.java) } - @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_numberOfChipsChanges_chipsGetSquishedAndUnsquished() = kosmos.runTest { @@ -425,7 +424,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } - @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_twoChips_isLandscape_notSquished() = kosmos.runTest { @@ -448,7 +447,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } - @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_twoChips_isLandscape_notSquished() = kosmos.runTest { @@ -471,7 +470,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } - @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_twoChips_isLargeScreen_notSquished() = kosmos.runTest { @@ -490,7 +489,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } - @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_twoChips_isLargeScreen_notSquished() = kosmos.runTest { @@ -522,7 +521,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsScreenRecordChip(latest) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_screenRecordShowAndShareToAppShow_primaryIsScreenRecordSecondaryIsHidden() = kosmos.runTest { @@ -542,7 +541,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_screenRecordAndShareToApp_screenRecordIsActiveShareToAppIsInOverflow() = kosmos.runTest { @@ -577,7 +576,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsShareToAppChip(latest) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_shareToAppShowAndCallShow_primaryIsShareToAppSecondaryIsCall() = kosmos.runTest { @@ -595,7 +594,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_shareToAppAndCallActive() = kosmos.runTest { @@ -631,7 +630,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsCallChip(latest, callNotificationKey) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_onlyCallShown_primaryIsCallSecondaryIsHidden() = kosmos.runTest { @@ -651,7 +650,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_callActive_restInactive() = kosmos.runTest { @@ -671,7 +670,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_singlePromotedNotif_primaryIsNotifSecondaryIsHidden() = kosmos.runTest { @@ -695,7 +694,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_singlePromotedNotif() = kosmos.runTest { @@ -720,7 +719,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_twoPromotedNotifs_primaryAndSecondaryAreNotifsInOrder() = kosmos.runTest { @@ -751,7 +750,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_twoPromotedNotifs_bothActiveInOrder() = kosmos.runTest { @@ -785,7 +784,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_threePromotedNotifs_topTwoShown() = kosmos.runTest { @@ -823,7 +822,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_threePromotedNotifs_topTwoActiveThirdInOverflow() = kosmos.runTest { @@ -865,7 +864,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_callAndPromotedNotifs_primaryIsCallSecondaryIsNotif() = kosmos.runTest { @@ -898,7 +897,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_callAndPromotedNotifs_callAndFirstNotifActiveSecondNotifInOverflow() = kosmos.runTest { @@ -935,7 +934,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_screenRecordAndCallAndPromotedNotifs_notifsNotShown() = kosmos.runTest { @@ -958,7 +957,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_screenRecordAndCallAndPromotedNotif_notifInOverflow() = kosmos.runTest { @@ -1076,7 +1075,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsNotifChip(latest, context, notifIcon, "notif") } - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_movesChipsAroundAccordingToPriority() = kosmos.runTest { @@ -1152,7 +1151,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_movesChipsAroundAccordingToPriority() = kosmos.runTest { @@ -1291,7 +1290,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { } /** Regression test for b/347726238. */ - @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @DisableChipsModernization @Test fun chipsLegacy_timerDoesNotResetAfterSubscribersRestart() = kosmos.runTest { @@ -1327,7 +1326,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { } /** Regression test for b/347726238. */ - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization @Test fun chips_timerDoesNotResetAfterSubscribersRestart() = kosmos.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt index ab475c5edb76..2f6bedb42e45 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt @@ -37,6 +37,8 @@ import com.android.systemui.statusbar.layout.LetterboxAppearance import com.android.systemui.statusbar.layout.LetterboxAppearanceCalculator import com.android.systemui.statusbar.layout.StatusBarBoundsProvider import com.android.systemui.statusbar.phone.fragment.dagger.HomeStatusBarComponent +import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel @@ -387,6 +389,7 @@ class StatusBarModeRepositoryImplTest : SysuiTestCase() { } @Test + @DisableChipsModernization fun statusBarMode_ongoingCallAndFullscreen_semiTransparent() = testScope.runTest { val latest by collectLastValue(underTest.statusBarAppearance) @@ -398,6 +401,19 @@ class StatusBarModeRepositoryImplTest : SysuiTestCase() { } @Test + @EnableChipsModernization + fun statusBarMode_ongoingProcessRequiresStatusBarVisible_andFullscreen_semiTransparent() = + testScope.runTest { + val latest by collectLastValue(underTest.statusBarAppearance) + + underTest.setOngoingProcessRequiresStatusBarVisible(true) + onSystemBarAttributesChanged(requestedVisibleTypes = WindowInsets.Type.navigationBars()) + + assertThat(latest!!.mode).isEqualTo(StatusBarMode.SEMI_TRANSPARENT) + } + + @Test + @DisableChipsModernization fun statusBarMode_ongoingCallButNotFullscreen_matchesAppearance() = testScope.runTest { val latest by collectLastValue(underTest.statusBarAppearance) @@ -413,6 +429,23 @@ class StatusBarModeRepositoryImplTest : SysuiTestCase() { } @Test + @EnableChipsModernization + fun statusBarMode_ongoingProcessRequiresStatusBarVisible_butNotFullscreen_matchesAppearance() = + testScope.runTest { + val latest by collectLastValue(underTest.statusBarAppearance) + + underTest.setOngoingProcessRequiresStatusBarVisible(true) + + onSystemBarAttributesChanged( + requestedVisibleTypes = WindowInsets.Type.statusBars(), + appearance = APPEARANCE_OPAQUE_STATUS_BARS, + ) + + assertThat(latest!!.mode).isEqualTo(StatusBarMode.OPAQUE) + } + + @Test + @DisableChipsModernization fun statusBarMode_fullscreenButNotOngoingCall_matchesAppearance() = testScope.runTest { val latest by collectLastValue(underTest.statusBarAppearance) @@ -427,6 +460,21 @@ class StatusBarModeRepositoryImplTest : SysuiTestCase() { } @Test + @EnableChipsModernization + fun statusBarMode_fullscreen_butNotOngoingProcessRequiresStatusBarVisible_matchesAppearance() = + testScope.runTest { + val latest by collectLastValue(underTest.statusBarAppearance) + + underTest.setOngoingProcessRequiresStatusBarVisible(false) + onSystemBarAttributesChanged( + requestedVisibleTypes = WindowInsets.Type.navigationBars(), + appearance = APPEARANCE_OPAQUE_STATUS_BARS, + ) + + assertThat(latest!!.mode).isEqualTo(StatusBarMode.OPAQUE) + } + + @Test fun statusBarMode_transientShown_semiTransparent() = testScope.runTest { val latest by collectLastValue(underTest.statusBarAppearance) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt index 426af264da07..83e26c4220b1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection +import android.app.Notification +import android.graphics.Color import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.TestableLooper.RunWithLooper @@ -72,4 +74,35 @@ class BundleEntryTest : SysuiTestCase() { fun getKey_adapter() { assertThat(underTest.entryAdapter.key).isEqualTo("key") } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isClearable_adapter() { + assertThat(underTest.entryAdapter.isClearable).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getSummarization_adapter() { + assertThat(underTest.entryAdapter.summarization).isNull() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getContrastedColor_adapter() { + assertThat(underTest.entryAdapter.getContrastedColor(context, false, Color.WHITE)) + .isEqualTo(Notification.COLOR_DEFAULT) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun canPeek_adapter() { + assertThat(underTest.entryAdapter.canPeek()).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getWhen_adapter() { + assertThat(underTest.entryAdapter.`when`).isEqualTo(0) + } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index 19d1224a9bf3..1f5c6722f38e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -163,9 +163,10 @@ public class NotificationEntryTest extends SysuiTestCase { @Test public void testIsExemptFromDndVisualSuppression_media() { + MediaSession session = new MediaSession(mContext, "test"); Notification.Builder n = new Notification.Builder(mContext, "") .setStyle(new Notification.MediaStyle() - .setMediaSession(mock(MediaSession.Token.class))) + .setMediaSession(session.getSessionToken())) .setSmallIcon(R.drawable.ic_person) .setContentTitle("Title") .setContentText("Text"); @@ -593,6 +594,76 @@ public class NotificationEntryTest extends SysuiTestCase { assertThat(entry.getEntryAdapter().getGroupRoot()).isEqualTo(parent.getEntryAdapter()); } + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void isClearable_adapter() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().isClearable()).isEqualTo(entry.isClearable()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getSummarization_adapter() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + Ranking ranking = new RankingBuilder(entry.getRanking()) + .setSummarization("hello") + .build(); + entry.setRanking(ranking); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().getSummarization()).isEqualTo("hello"); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getIcons_adapter() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().getIcons()).isEqualTo(entry.getIcons()); + } + private Notification.Action createContextualAction(String title) { return new Notification.Action.Builder( Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt index 7781df1ad91f..43cb9575b609 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt @@ -16,8 +16,11 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification +import android.app.Notification.MediaStyle +import android.media.session.MediaSession import android.platform.test.flag.junit.FlagsParameterization import android.provider.Settings +import android.service.notification.StatusBarNotification import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.dumpManager @@ -36,6 +39,7 @@ import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.setTransition import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder @@ -217,6 +221,16 @@ class OriginalUnseenKeyguardCoordinatorTest(flags: FlagsParameterization) : Sysu mock<ExpandableNotificationRow>().apply { whenever(isMediaRow).thenReturn(true) } + sbn = SbnBuilder().setNotification( + Notification.Builder(context, "channel").setStyle( + MediaStyle().setMediaSession( + MediaSession( + context, + "tag" + ).sessionToken + ) + ).build() + ).build() } collectionListener.onEntryAdded(fakeEntry) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt index 4c1f4f17e00c..1b8d64d5483c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt @@ -80,9 +80,6 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.testKosmos import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.wmshell.BubblesManager -import java.util.Optional -import kotlin.test.assertNotNull -import kotlin.test.fail import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -110,6 +107,9 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +import java.util.Optional +import kotlin.test.assertNotNull +import kotlin.test.fail /** Tests for [NotificationGutsManager]. */ @SmallTest @@ -509,7 +509,6 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( .setImportance(NotificationManager.IMPORTANCE_HIGH) .build() - whenever(row.isNonblockable).thenReturn(false) whenever(highPriorityProvider.isHighPriority(entry)).thenReturn(true) val statusBarNotification = entry.sbn gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -546,7 +545,6 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE) .build() - whenever(row.isNonblockable).thenReturn(false) val statusBarNotification = row.entry.sbn val entry = row.entry @@ -586,7 +584,6 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE) .build() - whenever(row.isNonblockable).thenReturn(false) val statusBarNotification = row.entry.sbn val entry = row.entry @@ -641,7 +638,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( ): NotificationMenuRowPlugin.MenuItem { val menuRow: NotificationMenuRowPlugin = NotificationMenuRow(mContext, peopleNotificationIdentifier) - menuRow.createMenu(row, row.entry.sbn) + menuRow.createMenu(row) val menuItem = menuRow.getLongpressMenuItem(mContext) assertNotNull(menuItem) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java index 027e899e20df..9fdfca14a5b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java @@ -72,7 +72,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { public void testAttachDetach() { NotificationMenuRowPlugin row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); ViewUtils.attachView(row.getMenuView()); TestableLooper.get(this).processAllMessages(); ViewUtils.detachView(row.getMenuView()); @@ -83,9 +83,9 @@ public class NotificationMenuRowTest extends LeakCheckedTest { public void testRecreateMenu() { NotificationMenuRowPlugin row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); assertTrue(row.getMenuView() != null); - row.createMenu(mRow, null); + row.createMenu(mRow); assertTrue(row.getMenuView() != null); } @@ -103,7 +103,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0); NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); ViewGroup container = (ViewGroup) row.getMenuView(); // noti blocking @@ -116,7 +116,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0); NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); ViewGroup container = (ViewGroup) row.getMenuView(); // just for noti blocking @@ -129,7 +129,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0); NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); ViewGroup container = (ViewGroup) row.getMenuView(); // one for snooze and one for noti blocking @@ -142,7 +142,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 1); NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow, null); + row.createMenu(mRow); ViewGroup container = (ViewGroup) row.getMenuView(); // Clear menu @@ -417,7 +417,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { public void testOnTouchMove() { NotificationMenuRow row = Mockito.spy( new NotificationMenuRow(mContext, mPeopleNotificationIdentifier)); - row.createMenu(mRow, null); + row.createMenu(mRow); doReturn(50f).when(row).getDismissThreshold(); doReturn(true).when(row).canBeDismissed(); doReturn(mView).when(row).getMenuView(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt index 9a6a6997b96f..081f52c4ff3b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt @@ -135,6 +135,7 @@ class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() { .thenReturn(mock()) whenever(requireViewById<View>(R.id.app_name_text)).thenReturn(mock()) whenever(requireViewById<View>(R.id.conversation_text)).thenReturn(mock()) + whenever(requireViewById<View>(R.id.title)).thenReturn(mock()) } return mockView } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt index e5cb0fbc9e4b..885e71e7a7fe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.testKosmos import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -49,7 +50,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { private val sectionsManager = mock<NotificationSectionsManager>() private val msdlPlayer = kosmos.fakeMSDLPlayer private var canRowBeDismissed = true - private var magneticAnimationsCancelled = false + private var magneticAnimationsCancelled = MutableList(childrenNumber) { false } private val underTest = kosmos.magneticNotificationRowManagerImpl @@ -64,8 +65,10 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { NotificationTestHelper(mContext, mDependency, kosmos.testableLooper, featureFlags) children = notificationTestHelper.createGroup(childrenNumber).childrenContainer swipedRow = children.attachedChildren[childrenNumber / 2] - configureMagneticRowListener(swipedRow) - magneticAnimationsCancelled = false + children.attachedChildren.forEachIndexed { index, row -> + row.magneticRowListener = row.magneticRowListener.asTestableListener(index) + } + magneticAnimationsCancelled.replaceAll { false } } @Test @@ -259,14 +262,14 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { underTest.onMagneticInteractionEnd(swipedRow, velocity = null) // THEN magnetic animations are cancelled - assertThat(magneticAnimationsCancelled).isTrue() + assertThat(magneticAnimationsCancelled[childrenNumber / 2]).isTrue() } @Test fun onMagneticInteractionEnd_forMagneticNeighbor_cancelsMagneticAnimations() = kosmos.testScope.runTest { - val neighborRow = children.attachedChildren[childrenNumber / 2 - 1] - configureMagneticRowListener(neighborRow) + val neighborIndex = childrenNumber / 2 - 1 + val neighborRow = children.attachedChildren[neighborIndex] // GIVEN that targets are set setTargets() @@ -275,9 +278,15 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { underTest.onMagneticInteractionEnd(neighborRow, null) // THEN magnetic animations are cancelled - assertThat(magneticAnimationsCancelled).isTrue() + assertThat(magneticAnimationsCancelled[neighborIndex]).isTrue() } + @After + fun tearDown() { + // We reset the manager so that all MagneticRowListener can cancel all animations + underTest.reset() + } + private fun setDetachedState() { val threshold = 100f underTest.setSwipeThresholdPx(threshold) @@ -302,27 +311,33 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { originalTranslation * MagneticNotificationRowManagerImpl.MAGNETIC_REDUCTION - private fun configureMagneticRowListener(row: ExpandableNotificationRow) { - val listener = - object : MagneticRowListener { - override fun setMagneticTranslation(translation: Float) { - row.translation = translation - } + private fun MagneticRowListener.asTestableListener(rowIndex: Int): MagneticRowListener { + val delegate = this + return object : MagneticRowListener { + override fun setMagneticTranslation(translation: Float) { + delegate.setMagneticTranslation(translation) + } - override fun triggerMagneticForce( - endTranslation: Float, - springForce: SpringForce, - startVelocity: Float, - ) {} + override fun triggerMagneticForce( + endTranslation: Float, + springForce: SpringForce, + startVelocity: Float, + ) { + delegate.triggerMagneticForce(endTranslation, springForce, startVelocity) + } - override fun cancelMagneticAnimations() { - magneticAnimationsCancelled = true - } + override fun cancelMagneticAnimations() { + magneticAnimationsCancelled[rowIndex] = true + delegate.cancelMagneticAnimations() + } - override fun cancelTranslationAnimations() {} + override fun cancelTranslationAnimations() { + delegate.cancelTranslationAnimations() + } - override fun canRowBeDismissed(): Boolean = canRowBeDismissed + override fun canRowBeDismissed(): Boolean { + return canRowBeDismissed } - row.magneticRowListener = listener + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 01ba4df3a314..7603eecd27ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -146,6 +146,20 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @EnableSceneContainer + fun resetViewStates_defaultHun_hasShadow() { + val headsUpTop = 200f + ambientState.headsUpTop = headsUpTop + + whenever(notificationRow.isPinned).thenReturn(true) + whenever(notificationRow.isHeadsUp).thenReturn(true) + + stackScrollAlgorithm.resetViewStates(ambientState, 0) + + assertThat(notificationRow.viewState.zTranslation).isGreaterThan(baseZ) + } + + @Test @DisableSceneContainer fun resetViewStates_defaultHunWhenShadeIsOpening_yTranslationIsInset() { whenever(notificationRow.isPinned).thenReturn(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 21297e3e64b7..c630a1c1e006 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -17,14 +17,15 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest -import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository -import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -54,11 +55,14 @@ import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.Transition import com.android.systemui.scene.data.repository.setTransition +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.enableSingleShade @@ -125,7 +129,6 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S kosmos.sharedNotificationContainerInteractor } private val largeScreenHeaderHelper by lazy { kosmos.mockLargeScreenHeaderHelper } - private val communalSceneRepository by lazy { kosmos.communalSceneRepository } lateinit var underTest: SharedNotificationContainerViewModel @@ -591,6 +594,25 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S } @Test + @DisableSceneContainer + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun isOnLockscreenFalseWhenCommunalShowing() = + kosmos.runTest { + val isOnLockscreen by collectLastValue(underTest.isOnLockscreen) + + setTransition( + sceneTransition = Idle(Scenes.Bouncer), + stateTransition = TransitionStep(from = LOCKSCREEN, to = PRIMARY_BOUNCER), + ) + assertThat(isOnLockscreen).isTrue() + + testScope.showCommunalScene() + + // If bouncer is showing over the hub, it should not be considered on lockscreen + assertThat(isOnLockscreen).isFalse() + } + + @Test fun isOnLockscreenWithoutShade() = testScope.runTest { val isOnLockscreenWithoutShade by collectLastValue(underTest.isOnLockscreenWithoutShade) @@ -1472,20 +1494,24 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S } private fun TestScope.showCommunalScene() { - val transitionState = - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(CommunalScenes.Communal) - ) - communalSceneRepository.setTransitionState(transitionState) + val targetScene = + if (SceneContainerFlag.isEnabled) { + Scenes.Communal + } else { + CommunalScenes.Communal + } + kosmos.communalSceneInteractor.changeScene(targetScene, "test") runCurrent() } private fun TestScope.hideCommunalScene() { - val transitionState = - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(CommunalScenes.Blank) - ) - communalSceneRepository.setTransitionState(transitionState) + val targetScene = + if (SceneContainerFlag.isEnabled) { + Scenes.Lockscreen + } else { + CommunalScenes.Blank + } + kosmos.communalSceneInteractor.changeScene(targetScene, "test") runCurrent() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt index 9d17348d95a0..bab349aa7a74 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.phone.ongoingcall.domain.interactor import android.app.PendingIntent -import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -29,12 +28,10 @@ import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel -import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState @@ -52,10 +49,9 @@ import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) -@EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) +@EnableChipsModernization class OngoingCallInteractorTest : SysuiTestCase() { private val kosmos = Kosmos().useUnconfinedTestDispatcher() - private val repository = kosmos.activeNotificationListRepository private val underTest = kosmos.ongoingCallInteractor @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index d17abd7da05c..6066a3870dfe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.os.ParcelUuid +import android.platform.test.annotations.EnableFlags import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET @@ -33,9 +34,12 @@ import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryLogbufferName import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot @@ -897,6 +901,71 @@ class MobileIconsInteractorTest : SysuiTestCase() { assertThat(latest).isEqualTo(2) } + @Test + @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun isStackable_tracksNumberOfSubscriptions() = + kosmos.runTest { + val latest by collectLastValue(underTest.isStackable) + + connectionsRepository.setSubscriptions(listOf(SUB_1)) + assertThat(latest).isFalse() + + connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) + assertThat(latest).isTrue() + + connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2, SUB_3_OPP)) + assertThat(latest).isFalse() + } + + @Test + @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun isStackable_checksForTerrestrialConnections() = + kosmos.runTest { + val exclusivelyNonTerrestrialSub = + SubscriptionModel( + isExclusivelyNonTerrestrial = true, + subscriptionId = 5, + carrierName = "Carrier 5", + profileClass = PROFILE_CLASS_UNSET, + ) + + val latest by collectLastValue(underTest.isStackable) + + connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) + assertThat(latest).isTrue() + + connectionsRepository.setSubscriptions(listOf(SUB_1, exclusivelyNonTerrestrialSub)) + assertThat(latest).isFalse() + } + + @Test + @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun isStackable_checksForNumberOfBars() = + kosmos.runTest { + val latest by collectLastValue(underTest.isStackable) + + // Number of levels is the same for both + connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) + setNumberOfLevelsForSubId(SUB_1_ID, 5) + setNumberOfLevelsForSubId(SUB_2_ID, 5) + + assertThat(latest).isTrue() + + // Change the number of levels to be different than SUB_2 + setNumberOfLevelsForSubId(SUB_1_ID, 6) + + assertThat(latest).isFalse() + } + + private fun setNumberOfLevelsForSubId(subId: Int, numberOfLevels: Int) { + with(kosmos) { + (fakeMobileConnectionsRepository.getRepoForSubId(subId) + as FakeMobileConnectionRepository) + .numberOfLevels + .value = numberOfLevels + } + } + /** * Convenience method for creating a pair of subscriptions to test the filteredSubscriptions * flow. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelTest.kt new file mode 100644 index 000000000000..20bdebd069f7 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2025 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.pipeline.mobile.ui.viewmodel + +import android.platform.test.annotations.EnableFlags +import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class StackedMobileIconViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val testScope = kosmos.testScope + + private val Kosmos.underTest: StackedMobileIconViewModel by Fixture { + stackedMobileIconViewModel + } + + @Before + fun setUp() { + kosmos.fakeFeatureFlagsClassic.set(Flags.NEW_NETWORK_SLICE_UI, false) + kosmos.underTest.activateIn(testScope) + } + + @Test + @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun dualSim_filtersOutNonDualConnections() = + kosmos.runTest { + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf() + assertThat(underTest.dualSim).isNull() + + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) + assertThat(underTest.dualSim).isNull() + + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3) + assertThat(underTest.dualSim).isNull() + + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + assertThat(underTest.dualSim).isNotNull() + } + + @Test + @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun dualSim_filtersOutNonCellularIcons() = + kosmos.runTest { + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) + assertThat(underTest.dualSim).isNull() + + fakeMobileIconsInteractor + .getInteractorForSubId(SUB_1.subscriptionId)!! + .signalLevelIcon + .value = + SignalIconModel.Satellite( + level = 0, + icon = Icon.Resource(res = 0, contentDescription = null), + ) + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + assertThat(underTest.dualSim).isNull() + } + + @Test + @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun dualSim_tracksActiveSubId() = + kosmos.runTest { + // Active sub id is null, order is unchanged + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + setIconLevel(SUB_1.subscriptionId, 1) + setIconLevel(SUB_2.subscriptionId, 2) + + assertThat(underTest.dualSim!!.primary.level).isEqualTo(1) + assertThat(underTest.dualSim!!.secondary.level).isEqualTo(2) + + // Active sub is 2, order is swapped + fakeMobileIconsInteractor.activeMobileDataSubscriptionId.value = SUB_2.subscriptionId + + assertThat(underTest.dualSim!!.primary.level).isEqualTo(2) + assertThat(underTest.dualSim!!.secondary.level).isEqualTo(1) + } + + private fun setIconLevel(subId: Int, level: Int) { + with(kosmos.fakeMobileIconsInteractor.getInteractorForSubId(subId)!!) { + signalLevelIcon.value = + (signalLevelIcon.value as SignalIconModel.Cellular).copy(level = level) + } + } + + companion object { + private val SUB_1 = + SubscriptionModel( + subscriptionId = 1, + isOpportunistic = false, + carrierName = "Carrier 1", + profileClass = PROFILE_CLASS_UNSET, + ) + private val SUB_2 = + SubscriptionModel( + subscriptionId = 2, + isOpportunistic = false, + carrierName = "Carrier 2", + profileClass = PROFILE_CLASS_UNSET, + ) + private val SUB_3 = + SubscriptionModel( + subscriptionId = 3, + isOpportunistic = false, + carrierName = "Carrier 3", + profileClass = PROFILE_CLASS_UNSET, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt index eb961bd5f4ae..f91e3a612862 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt @@ -64,6 +64,8 @@ class FakeHomeStatusBarViewModel( override val isHomeStatusBarAllowedByScene = MutableStateFlow(false) + override val canShowOngoingActivityChips: Flow<Boolean> = MutableStateFlow(false) + override val batteryViewModelFactory: BatteryViewModel.Factory = object : BatteryViewModel.Factory { override fun create(): BatteryViewModel = mock(BatteryViewModel::class.java) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt index cd8ca23e0964..8a796fc33608 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt @@ -86,6 +86,7 @@ import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataS import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher import com.android.systemui.statusbar.phone.data.repository.fakeDarkIconRepository +import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.pipeline.shared.domain.interactor.setHomeStatusBarIconBlockList import com.android.systemui.statusbar.pipeline.shared.domain.interactor.setHomeStatusBarInteractorShowOperatorName @@ -677,6 +678,60 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test + fun canShowOngoingActivityChips_statusBarHidden_noSecureCamera_noHun_false() = + kosmos.runTest { + val latest by collectLastValue(underTest.canShowOngoingActivityChips) + + // home status bar not allowed + kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen) + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false, taskInfo = null) + + assertThat(latest).isFalse() + } + + @Test + fun canShowOngoingActivityChips_statusBarNotHidden_noSecureCamera_noHun_true() = + kosmos.runTest { + val latest by collectLastValue(underTest.canShowOngoingActivityChips) + + transitionKeyguardToGone() + + assertThat(latest).isTrue() + } + + @Test + fun canShowOngoingActivityChips_statusBarNotHidden_secureCamera_noHun_false() = + kosmos.runTest { + val latest by collectLastValue(underTest.canShowOngoingActivityChips) + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + testScope = testScope, + ) + kosmos.keyguardInteractor.onCameraLaunchDetected(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) + + assertThat(latest).isFalse() + } + + @Test + fun canShowOngoingActivityChips_statusBarNotHidden_noSecureCamera_hun_false() = + kosmos.runTest { + val latest by collectLastValue(underTest.canShowOngoingActivityChips) + + transitionKeyguardToGone() + + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + assertThat(latest).isFalse() + } + + @Test fun isClockVisible_allowedByDisableFlags_visible() = kosmos.runTest { val latest by collectLastValue(underTest.isClockVisible) @@ -835,7 +890,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + @EnableChipsModernization fun isNotificationIconContainerVisible_anyChipShowing_ChipsModernizationOn() = kosmos.runTest { val latest by collectLastValue(underTest.isNotificationIconContainerVisible) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/RotationLockControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/RotationLockControllerImplTest.java index 8593f6a08b5a..605e4a47275b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/RotationLockControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/RotationLockControllerImplTest.java @@ -37,15 +37,19 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Optional; + @RunWith(AndroidJUnit4.class) @TestableLooper.RunWithLooper @SmallTest public class RotationLockControllerImplTest extends SysuiTestCase { - private static final String[] DEFAULT_SETTINGS = new String[] {"0:0", "1:2"}; + private static final String[] DEFAULT_SETTINGS = new String[]{"0:0", "1:2"}; - @Mock RotationPolicyWrapper mRotationPolicyWrapper; - @Mock DeviceStateRotationLockSettingController mDeviceStateRotationLockSettingController; + @Mock + RotationPolicyWrapper mRotationPolicyWrapper; + @Mock + DeviceStateRotationLockSettingController mDeviceStateRotationLockSettingController; private ArgumentCaptor<RotationPolicy.RotationPolicyListener> mRotationPolicyListenerCaptor; @@ -93,7 +97,7 @@ public class RotationLockControllerImplTest extends SysuiTestCase { private void createRotationLockController(String[] deviceStateRotationLockDefaults) { new RotationLockControllerImpl( mRotationPolicyWrapper, - mDeviceStateRotationLockSettingController, + Optional.of(mDeviceStateRotationLockSettingController), deviceStateRotationLockDefaults); } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java index d3218ad8c9fb..a0bf7f00b11c 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java @@ -203,6 +203,8 @@ public interface QS extends FragmentBase { */ void setIsNotificationPanelFullWidth(boolean isFullWidth); + default void setQSExpandingOrCollapsing(boolean isQSExpandingOrCollapsing) {} + /** * Callback for when QSPanel container is scrolled */ diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java index 94fdbae83253..9b961d2535ae 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java @@ -122,7 +122,7 @@ public interface NotificationMenuRowPlugin extends Plugin { public void setAppName(String appName); - public void createMenu(ViewGroup parent, StatusBarNotification sbn); + public void createMenu(ViewGroup parent); public void resetMenu(); @@ -215,9 +215,8 @@ public interface NotificationMenuRowPlugin extends Plugin { /** * Callback used to signal the menu that its parent notification has been updated. - * @param sbn */ - public void onNotificationUpdated(StatusBarNotification sbn); + public void onNotificationUpdated(); /** * Callback used to signal the menu that a user is moving the parent notification. diff --git a/packages/SystemUI/res/color/media_player_outline_button_bg.xml b/packages/SystemUI/res/color/media_player_outline_button_bg.xml deleted file mode 100644 index ba7848a5d23e..000000000000 --- a/packages/SystemUI/res/color/media_player_outline_button_bg.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<selector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> - <item android:color="?androidprv:attr/colorAccentPrimaryVariant"/> -</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/color/media_player_solid_button_bg.xml b/packages/SystemUI/res/color/media_player_solid_button_bg.xml index 69c971188d34..cc54fa3162cc 100644 --- a/packages/SystemUI/res/color/media_player_solid_button_bg.xml +++ b/packages/SystemUI/res/color/media_player_solid_button_bg.xml @@ -15,7 +15,6 @@ ~ limitations under the License. --> -<selector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> - <item android:color="?androidprv:attr/colorAccentPrimary"/> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@android:color/system_primary_dark"/> </selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/qs_media_art_background.xml b/packages/SystemUI/res/drawable/qs_media_art_background.xml index 95a187094113..e59f82b77908 100644 --- a/packages/SystemUI/res/drawable/qs_media_art_background.xml +++ b/packages/SystemUI/res/drawable/qs_media_art_background.xml @@ -15,6 +15,7 @@ ~ limitations under the License --> <shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <corners android:radius="@dimen/qs_media_album_radius"/> + android:shape="rectangle"> + <solid android:color="#FF000000" /> + <corners android:radius="@dimen/notification_corner_radius"/> </shape> diff --git a/packages/SystemUI/res/layout/bindable_status_bar_compose_icon.xml b/packages/SystemUI/res/layout/bindable_status_bar_compose_icon.xml index fa9318bc151c..6b55ac2e0398 100644 --- a/packages/SystemUI/res/layout/bindable_status_bar_compose_icon.xml +++ b/packages/SystemUI/res/layout/bindable_status_bar_compose_icon.xml @@ -27,7 +27,6 @@ android:layout_height="@dimen/status_bar_bindable_icon_size" android:layout_width="wrap_content" android:layout_gravity="center_vertical" - android:padding="4sp" /> </com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 8665fd6dcaf5..f4c6904028ca 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -198,6 +198,7 @@ <!-- media --> <color name="media_seamless_border">?android:attr/colorAccent</color> <color name="media_paging_indicator">@color/material_dynamic_neutral_variant80</color> + <color name="media_on_background">#FFFFFF</color> <!-- media output dialog--> <color name="media_dialog_background" android:lstar="98">@color/material_dynamic_neutral90</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 17a89b3a0394..640e1fa79530 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -491,6 +491,9 @@ <!-- some constraints use a negative margin. must be aligned with overlay_border_width, above; overlay_border_width_neg = overlay_border_width * -1 --> <dimen name="overlay_border_width_neg">-4dp</dimen> + <dimen name="overlay_shade_panel_shape_radius"> + @dimen/aux_spacing_overlay_panel_shape_radius + </dimen> <dimen name="clipboard_preview_size">@dimen/overlay_x_scale</dimen> <dimen name="clipboard_overlay_min_font">10sp</dimen> @@ -2215,4 +2218,8 @@ <dimen name="rear_display_progress_width">231dp</dimen> <!-- Rear display mode end --> + <!-- Spacing attributes to overwrite --> + <dimen name="aux_spacing_overlay_panel_shape_radius">46dp</dimen> + <!-- Spacing attributes to overwrite end --> + </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 87c42824885d..359bd2bcb37c 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2560,6 +2560,9 @@ <!-- SysUI Tuner: Other section --> <string name="other">Other</string> + <!-- Accessibility description of action to toggle QS tile size on click. It will read as "Double-tap to toggle the tile's size" in screen readers [CHAR LIMIT=NONE] --> + <string name="accessibility_qs_edit_toggle_tile_size_action">toggle the tile\'s size</string> + <!-- Accessibility description of action to remove QS tile on click. It will read as "Double-tap to remove tile" in screen readers [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_remove_tile_action">remove tile</string> @@ -3167,8 +3170,8 @@ <string name="controls_media_settings_button">Settings</string> <!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]--> <string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string> - <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] --> - <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string> + <!-- Content description for media controls progress bar [CHAR_LIMIT=NONE] --> + <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1 hour 2 minutes 30 seconds">%1$s</xliff:g> of <xliff:g id="total_time" example="4 hours 5 seconds">%2$s</xliff:g></string> <!-- Placeholder title to inform user that an app has posted media controls [CHAR_LIMIT=NONE] --> <string name="controls_media_empty_title"><xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g> is running</string> @@ -4097,8 +4100,10 @@ <!-- Title of the one line view of a redacted notification --> <string name="redacted_notification_single_line_title">Redacted</string> - <!-- Main text of the one line view of a redacted notification --> - <string name="redacted_notification_single_line_text">Unlock to view</string> + <!-- Main text of the one line view of a public notification --> + <string name="public_notification_single_line_text">Unlock to view</string> + <!-- Main text of the one line view of a redacted OTP notification --> + <string name="redacted_otp_notification_single_line_text">Unlock to view code</string> <!-- Content description for contextual education dialog [CHAR LIMIT=NONE] --> <string name="contextual_education_dialog_title">Contextual education</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 672c133269ec..8a6b3af172d7 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -828,23 +828,23 @@ <item name="android:elevation">10dp</item> </style> - <!-- Media controls always have light background --> + <!-- Media controls always have dark background --> <style name="MediaPlayer" parent="@*android:style/Theme.DeviceDefault.Light"> - <item name="android:textColor">?android:attr/textColorPrimary</item> - <item name="android:backgroundTint">@color/material_dynamic_secondary95</item> + <item name="android:textColor">@color/media_on_background</item> + <item name="android:backgroundTint">@android:color/system_on_surface_light</item> </style> <style name="MediaPlayer.ProgressBar" parent="@android:style/Widget.ProgressBar.Horizontal"> <item name="android:thumb">@drawable/media_seekbar_thumb</item> - <item name="android:thumbTint">?android:attr/textColorPrimary</item> + <item name="android:thumbTint">@color/media_on_background</item> <item name="android:progressDrawable">@drawable/media_squiggly_progress</item> - <item name="android:progressTint">?android:attr/textColorPrimary</item> - <item name="android:progressBackgroundTint">?android:attr/textColorTertiary</item> + <item name="android:progressTint">@color/media_on_background</item> + <item name="android:progressBackgroundTint">@android:color/system_primary_dark</item> <item name="android:splitTrack">false</item> </style> <style name="MediaPlayer.Subtitle" parent="MediaPlayer"> - <item name="android:textColor">?android:attr/textColorSecondary</item> + <item name="android:textColor">@color/media_on_background</item> </style> <style name="MediaPlayer.ScrubbingTime"> @@ -853,21 +853,10 @@ <item name="android:gravity">center</item> </style> - <style name="MediaPlayer.Action" parent="@android:style/Widget.Material.Button.Borderless.Small"> - <item name="android:background">@drawable/qs_media_light_source</item> - <item name="android:tint">?android:attr/textColorPrimary</item> - <item name="android:stateListAnimator">@anim/media_button_state_list_animator</item> - <item name="android:paddingTop">8dp</item> - <item name="android:paddingStart">12dp</item> - <item name="android:paddingEnd">12dp</item> - <item name="android:paddingBottom">16dp</item> - <item name="android:scaleType">centerInside</item> - </style> - <style name="MediaPlayer.SessionAction" parent="@android:style/Widget.Material.Button.Borderless.Small"> <item name="android:background">@drawable/qs_media_light_source</item> - <item name="android:tint">?android:attr/textColorPrimary</item> + <item name="android:tint">@color/media_on_background</item> <item name="android:paddingTop">12dp</item> <item name="android:paddingStart">12dp</item> <item name="android:paddingEnd">12dp</item> @@ -886,8 +875,8 @@ <style name="MediaPlayer.OutlineButton"> <item name="android:background">@drawable/qs_media_outline_button</item> - <item name="android:textColor">?android:attr/textColorPrimary</item> - <item name="android:backgroundTint">@color/media_player_outline_button_bg</item> + <item name="android:textColor">@color/media_on_background</item> + <item name="android:backgroundTint">@android:color/system_primary_dark</item> <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item> <item name="android:layout_gravity">center</item> <item name="android:singleLine">true</item> @@ -895,8 +884,8 @@ <style name="MediaPlayer.SolidButton"> <item name="android:backgroundTint">@color/media_player_solid_button_bg</item> - <item name="android:tint">?android:attr/colorPrimary</item> - <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:tint">@android:color/system_on_primary_dark</item> + <item name="android:textColor">@android:color/system_on_primary_dark</item> </style> <style name="MediaPlayer.Recommendation"/> diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 0f1da509468a..ae3a76e2d2ca 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -71,6 +71,7 @@ android_library { "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", "//frameworks/libs/systemui:msdl", "//frameworks/libs/systemui:view_capture", + "am_flags_lib", ], resource_dirs: [ "res", diff --git a/packages/SystemUI/shared/res/values/bools.xml b/packages/SystemUI/shared/res/values/bools.xml index f22dac4b9fb4..98e5cea0e78f 100644 --- a/packages/SystemUI/shared/res/values/bools.xml +++ b/packages/SystemUI/shared/res/values/bools.xml @@ -22,4 +22,7 @@ <resources> <!-- Whether to add padding at the bottom of the complication clock --> <bool name="dream_overlay_complication_clock_bottom_padding">false</bool> -</resources>
\ No newline at end of file + + <!-- Whether to mark tasks that are present in the UI as perceptible tasks. --> + <bool name="config_usePerceptibleTasks">false</bool> +</resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index ed9ba7aa455b..487d1ce2514e 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -46,6 +46,8 @@ import android.view.Display; import android.window.TaskSnapshot; import com.android.internal.app.IVoiceInteractionManagerService; +import com.android.server.am.Flags; +import com.android.systemui.shared.R; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -227,6 +229,17 @@ public class ActivityManagerWrapper { } /** + * Sets whether or not the specified task is perceptible. + */ + public boolean setTaskIsPerceptible(int taskId, boolean isPerceptible) { + try { + return getService().setTaskIsPerceptible(taskId, isPerceptible); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Removes a task by id. */ public void removeTask(final int taskId) { @@ -311,10 +324,23 @@ public class ActivityManagerWrapper { } /** + * Returns true if tasks with a presence in the UI should be marked as perceptible tasks. + */ + public static boolean usePerceptibleTasks(Context context) { + return Flags.perceptibleTasks() + && context.getResources().getBoolean(R.bool.config_usePerceptibleTasks); + } + + /** * Returns true if the running task represents the home task */ public static boolean isHomeTask(RunningTaskInfo info) { return info.configuration.windowConfiguration.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_HOME; } + + public boolean isRunningInTestHarness() { + return ActivityManager.isRunningInTestHarness() + || ActivityManager.isRunningInUserTestHarness(); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java index 5e36539ecbec..a7bb11ea0442 100644 --- a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java +++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java @@ -156,7 +156,7 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { mMainExecutor.execute(() -> mView.updateEmergencyCallButton( /* isInCall= */ isInCall, /* hasTelephonyRadio= */ getContext().getPackageManager() - .hasSystemFeature(PackageManager.FEATURE_TELEPHONY), + .hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING), /* simLocked= */ mKeyguardUpdateMonitor.isSimPinVoiceSecure(), /* isSecure= */ isSecure)); }); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java index 7c141c1b561e..5247acc94e03 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java @@ -58,11 +58,22 @@ public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi private final BiMap<Integer, AmbientVolumeSlider> mSideToSliderMap = HashBiMap.create(); private int mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; + private HearingDevicesUiEventLogger mUiEventLogger; + private int mLaunchSourceId; + private final AmbientVolumeSlider.OnChangeListener mSliderOnChangeListener = (slider, value) -> { - if (mListener != null) { - final int side = mSideToSliderMap.inverse().get(slider); - mListener.onSliderValueChange(side, value); + final Integer side = mSideToSliderMap.inverse().get(slider); + if (side != null) { + if (mUiEventLogger != null) { + HearingDevicesUiEvent uiEvent = side == SIDE_UNIFIED + ? HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_CHANGE_UNIFIED + : HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_CHANGE_SEPARATED; + mUiEventLogger.log(uiEvent, mLaunchSourceId); + } + if (mListener != null) { + mListener.onSliderValueChange(side, value); + } } }; @@ -94,6 +105,12 @@ public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi return; } setMuted(!mMuted); + if (mUiEventLogger != null) { + HearingDevicesUiEvent uiEvent = mMuted + ? HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_MUTE + : HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_UNMUTE; + mUiEventLogger.log(uiEvent, mLaunchSourceId); + } if (mListener != null) { mListener.onAmbientVolumeIconClick(); } @@ -103,6 +120,12 @@ public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi mExpandIcon = requireViewById(R.id.ambient_expand_icon); mExpandIcon.setOnClickListener(v -> { setExpanded(!mExpanded); + if (mUiEventLogger != null) { + HearingDevicesUiEvent uiEvent = mExpanded + ? HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_EXPAND_CONTROLS + : HearingDevicesUiEvent.HEARING_DEVICES_AMBIENT_COLLAPSE_CONTROLS; + mUiEventLogger.log(uiEvent, mLaunchSourceId); + } if (mListener != null) { mListener.onExpandIconClick(); } @@ -243,6 +266,11 @@ public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi updateVolumeLevel(); } + void setUiEventLogger(HearingDevicesUiEventLogger uiEventLogger, int launchSourceId) { + mUiEventLogger = uiEventLogger; + mLaunchSourceId = launchSourceId; + } + private void updateVolumeLevel() { int leftLevel, rightLevel; if (mExpanded) { 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 22ecb0af8c18..786d27af3994 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -382,6 +382,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private void setupAmbientControls(CachedBluetoothDevice activeHearingDevice) { final AmbientVolumeLayout ambientLayout = mDialog.requireViewById(R.id.ambient_layout); + ambientLayout.setUiEventLogger(mUiEventLogger, mLaunchSourceId); mAmbientController = new AmbientVolumeUiController( mDialog.getContext(), mLocalBluetoothManager, ambientLayout); mAmbientController.setShowUiWhenLocalDataExist(false); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt index 9e77b02be495..fe1d5040c6f5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt @@ -29,7 +29,17 @@ enum class HearingDevicesUiEvent(private val id: Int) : UiEventLogger.UiEventEnu @UiEvent(doc = "Click on the device gear to enter device detail page") HEARING_DEVICES_GEAR_CLICK(1853), @UiEvent(doc = "Select a preset from preset spinner") HEARING_DEVICES_PRESET_SELECT(1854), - @UiEvent(doc = "Click on related tool") HEARING_DEVICES_RELATED_TOOL_CLICK(1856); + @UiEvent(doc = "Click on related tool") HEARING_DEVICES_RELATED_TOOL_CLICK(1856), + @UiEvent(doc = "Change the ambient volume with unified control") + HEARING_DEVICES_AMBIENT_CHANGE_UNIFIED(2149), + @UiEvent(doc = "Change the ambient volume with separated control") + HEARING_DEVICES_AMBIENT_CHANGE_SEPARATED(2150), + @UiEvent(doc = "Mute the ambient volume") HEARING_DEVICES_AMBIENT_MUTE(2151), + @UiEvent(doc = "Unmute the ambient volume") HEARING_DEVICES_AMBIENT_UNMUTE(2152), + @UiEvent(doc = "Expand the ambient volume controls") + HEARING_DEVICES_AMBIENT_EXPAND_CONTROLS(2153), + @UiEvent(doc = "Collapse the ambient volume controls") + HEARING_DEVICES_AMBIENT_COLLAPSE_CONTROLS(2154); override fun getId(): Int = this.id } diff --git a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS index 429b4b0fccab..329aa07fdd9e 100644 --- a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS @@ -3,6 +3,5 @@ dupin@google.com linyuh@google.com pauldpong@google.com -praveenj@google.com vicliang@google.com yuklimko@google.com diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index b6537118324e..4c8a8f1c13d7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -33,6 +33,7 @@ import android.graphics.PixelFormat; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager.Authenticators; +import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.PromptInfo; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; @@ -100,7 +101,7 @@ import javax.inject.Provider; */ @Deprecated public class AuthContainerView extends LinearLayout - implements AuthDialog, WakefulnessLifecycle.Observer, CredentialView.Host { + implements WakefulnessLifecycle.Observer, CredentialView.Host { private static final String TAG = "AuthContainerView"; @@ -158,12 +159,11 @@ public class AuthContainerView extends LinearLayout private final Set<Integer> mFailedModalities = new HashSet<Integer>(); private final OnBackInvokedCallback mBackCallback = this::onBackInvoked; - private final @Background DelayableExecutor mBackgroundExecutor; private final MSDLPlayer mMSDLPlayer; // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet. - @Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason; + @Nullable @BiometricPrompt.DismissedReason private Integer mPendingCallbackReason; // HAT received from LockSettingsService when credential is verified. @Nullable private byte[] mCredentialAttestation; @@ -188,18 +188,18 @@ public class AuthContainerView extends LinearLayout final class BiometricCallback implements Spaghetti.Callback { @Override public void onAuthenticated() { - animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED); + animateAway(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED); } @Override public void onUserCanceled() { sendEarlyUserCanceled(); - animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); } @Override public void onButtonNegative() { - animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE); + animateAway(BiometricPrompt.DISMISSED_REASON_NEGATIVE); } @Override @@ -210,12 +210,12 @@ public class AuthContainerView extends LinearLayout @Override public void onContentViewMoreOptionsButtonPressed() { - animateAway(AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS); + animateAway(BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS); } @Override public void onError() { - animateAway(AuthDialogCallback.DISMISSED_ERROR); + animateAway(BiometricPrompt.DISMISSED_REASON_ERROR); } @Override @@ -234,20 +234,20 @@ public class AuthContainerView extends LinearLayout @Override public void onAuthenticatedAndConfirmed() { - animateAway(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE); + animateAway(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED); } } @Override public void onCredentialMatched(@NonNull byte[] attestation) { mCredentialAttestation = attestation; - animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); + animateAway(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED); } @Override public void onCredentialAborted() { sendEarlyUserCanceled(); - animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); } @Override @@ -277,7 +277,7 @@ public class AuthContainerView extends LinearLayout com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss, null /* OnClickListener */) .setOnDismissListener( - dialog -> animateAway(AuthDialogCallback.DISMISSED_ERROR)) + dialog -> animateAway(BiometricPrompt.DISMISSED_REASON_ERROR)) .create(); alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); alertDialog.show(); @@ -349,7 +349,6 @@ public class AuthContainerView extends LinearLayout mPanelView = mLayout.findViewById(R.id.panel); mPanelController = new AuthPanelController(mContext, mPanelView); - mBackgroundExecutor = bgExecutor; mInteractionJankMonitor = jankMonitor; mCredentialViewModelProvider = credentialViewModelProvider; @@ -394,7 +393,7 @@ public class AuthContainerView extends LinearLayout @VisibleForTesting public void onBackInvoked() { sendEarlyUserCanceled(); - animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); } void sendEarlyUserCanceled() { @@ -402,7 +401,6 @@ public class AuthContainerView extends LinearLayout BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL, getRequestId()); } - @Override public boolean isAllowDeviceCredentials() { return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo); } @@ -450,7 +448,6 @@ public class AuthContainerView extends LinearLayout mPanelController.setContainerDimensions(getMeasuredWidth(), getMeasuredHeight()); } - @Override public void onOrientationChanged() { } @@ -538,10 +535,9 @@ public class AuthContainerView extends LinearLayout @Override public void onStartedGoingToSleep() { - animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); } - @Override public void show(WindowManager wm) { wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle())); } @@ -559,7 +555,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public void dismissWithoutCallback(boolean animate) { if (animate) { animateAway(false /* sendReason */, 0 /* reason */); @@ -569,12 +564,10 @@ public class AuthContainerView extends LinearLayout } } - @Override public void dismissFromSystemServer() { animateAway(false /* sendReason */, 0 /* reason */); } - @Override public void onAuthenticationSucceeded(@Modality int modality) { if (mBiometricView != null) { mBiometricView.onAuthenticationSucceeded(modality); @@ -583,7 +576,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public void onAuthenticationFailed(@Modality int modality, String failureReason) { if (mBiometricView != null) { mFailedModalities.add(modality); @@ -593,7 +585,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public void onHelp(@Modality int modality, String help) { if (mBiometricView != null) { mBiometricView.onHelp(modality, help); @@ -602,7 +593,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public void onError(@Modality int modality, String error) { if (mBiometricView != null) { mBiometricView.onError(modality, error); @@ -611,7 +601,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public void onPointerDown() { if (mBiometricView != null) { if (mFailedModalities.contains(TYPE_FACE)) { @@ -624,22 +613,18 @@ public class AuthContainerView extends LinearLayout } } - @Override public String getOpPackageName() { return mConfig.mOpPackageName; } - @Override public String getClassNameIfItIsConfirmDeviceCredentialActivity() { return mConfig.mPromptInfo.getClassNameIfItIsConfirmDeviceCredentialActivity(); } - @Override public long getRequestId() { return mConfig.mRequestId; } - @Override public void animateToCredentialUI(boolean isError) { if (mBiometricView != null) { mBiometricView.startTransitionToCredentialUI(isError); @@ -648,11 +633,11 @@ public class AuthContainerView extends LinearLayout } } - void animateAway(@AuthDialogCallback.DismissedReason int reason) { + void animateAway(@BiometricPrompt.DismissedReason int reason) { animateAway(true /* sendReason */, reason); } - private void animateAway(boolean sendReason, @AuthDialogCallback.DismissedReason int reason) { + private void animateAway(boolean sendReason, @BiometricPrompt.DismissedReason int reason) { if (mContainerState == STATE_ANIMATING_IN) { Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn"); mContainerState = STATE_PENDING_DISMISS; @@ -732,7 +717,7 @@ public class AuthContainerView extends LinearLayout private void onDialogAnimatedIn() { if (mContainerState == STATE_PENDING_DISMISS) { Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now"); - animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL); return; } if (mContainerState == STATE_ANIMATING_OUT || mContainerState == STATE_GONE) { @@ -748,7 +733,6 @@ public class AuthContainerView extends LinearLayout } } - @Override public PromptViewModel getViewModel() { return mPromptViewModel; } @@ -776,7 +760,6 @@ public class AuthContainerView extends LinearLayout return lp; } - @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println(" isAttachedToWindow=" + isAttachedToWindow()); pw.println(" containerState=" + mContainerState); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index eee5f9e34317..68a282018ba4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -163,7 +163,7 @@ public class AuthController implements // TODO: These should just be saved from onSaveState private SomeArgs mCurrentDialogArgs; @VisibleForTesting - AuthDialog mCurrentDialog; + AuthContainerView mCurrentDialog; @NonNull private final WindowManager mWindowManager; @NonNull private final DisplayManager mDisplayManager; @@ -222,7 +222,7 @@ public class AuthController implements closeDialog(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, reasonString); } - private void closeDialog(@DismissedReason int reason, String reasonString) { + private void closeDialog(@BiometricPrompt.DismissedReason int reason, String reasonString) { if (isShowing()) { Log.i(TAG, "Close BP, reason :" + reasonString); mCurrentDialog.dismissWithoutCallback(true /* animate */); @@ -511,60 +511,14 @@ public class AuthController implements } @Override - public void onDismissed(@DismissedReason int reason, - @Nullable byte[] credentialAttestation, long requestId) { - + public void onDismissed(@BiometricPrompt.DismissedReason int reason, + @Nullable byte[] credentialAttestation, long requestId) { if (mCurrentDialog != null && requestId != mCurrentDialog.getRequestId()) { Log.w(TAG, "requestId doesn't match, skip onDismissed"); return; } - switch (reason) { - case AuthDialogCallback.DISMISSED_USER_CANCELED: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_NEGATIVE, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_BUTTON_POSITIVE: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED: - sendResultAndCleanUp( - BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_ERROR: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_ERROR, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_BY_SYSTEM_SERVER: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED: - sendResultAndCleanUp(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED, - credentialAttestation); - break; - - case AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS: - sendResultAndCleanUp( - BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS, - credentialAttestation); - break; - default: - Log.e(TAG, "Unhandled reason: " + reason); - break; - } + sendResultAndCleanUp(reason, credentialAttestation); } @Override @@ -699,7 +653,7 @@ public class AuthController implements mUdfpsController.onAodInterrupt(screenX, screenY, major, minor); } - private void sendResultAndCleanUp(@DismissedReason int reason, + private void sendResultAndCleanUp(@BiometricPrompt.DismissedReason int reason, @Nullable byte[] credentialAttestation) { if (mReceiver == null) { Log.e(TAG, "sendResultAndCleanUp: Receiver is null"); @@ -1244,7 +1198,7 @@ public class AuthController implements final long requestId = args.argl2; // Create a new dialog but do not replace the current one yet. - final AuthDialog newDialog = buildDialog( + final AuthContainerView newDialog = buildDialog( mBackgroundExecutor, promptInfo, requireConfirmation, @@ -1327,7 +1281,7 @@ public class AuthController implements return mContext.createDisplayContext(display).getSystemService(WindowManager.class); } - private void onDialogDismissed(@DismissedReason int reason) { + private void onDialogDismissed(@BiometricPrompt.DismissedReason int reason) { if (DEBUG) Log.d(TAG, "onDialogDismissed: " + reason); if (mCurrentDialog == null) { Log.w(TAG, "Dialog already dismissed"); @@ -1361,7 +1315,7 @@ public class AuthController implements } } - protected AuthDialog buildDialog(@Background DelayableExecutor bgExecutor, + protected AuthContainerView buildDialog(@Background DelayableExecutor bgExecutor, PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds, String opPackageName, boolean skipIntro, long operationId, long requestId, @NonNull WakefulnessLifecycle wakefulnessLifecycle, @@ -1389,7 +1343,7 @@ public class AuthController implements @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { - final AuthDialog dialog = mCurrentDialog; + final AuthContainerView dialog = mCurrentDialog; pw.println(" mCachedDisplayInfo=" + mCachedDisplayInfo); pw.println(" mScaleFactor=" + mScaleFactor); pw.println(" fingerprintSensorLocationInNaturalOrientation=" diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java deleted file mode 100644 index 861191671ba9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2019 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.biometrics; - -import android.hardware.biometrics.BiometricAuthenticator.Modality; -import android.view.WindowManager; - -import com.android.systemui.Dumpable; -import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel; - -/** - * Interface for the biometric dialog UI. - * - * TODO(b/287311775): remove along with legacy controller once flag is removed - */ -@Deprecated -public interface AuthDialog extends Dumpable { - - /** - * Parameters used when laying out {@link AuthBiometricView}, its subclasses, and - * {@link AuthPanelController}. - */ - class LayoutParams { - public final int mMediumHeight; - public final int mMediumWidth; - - public LayoutParams(int mediumWidth, int mediumHeight) { - mMediumWidth = mediumWidth; - mMediumHeight = mediumHeight; - } - } - - /** - * Show the dialog. - * @param wm - */ - void show(WindowManager wm); - - /** - * Dismiss the dialog without sending a callback. - */ - void dismissWithoutCallback(boolean animate); - - /** - * Dismiss the dialog. Animate away. - */ - void dismissFromSystemServer(); - - /** - * Biometric authenticated. May be pending user confirmation, or completed. - */ - void onAuthenticationSucceeded(@Modality int modality); - - /** - * Authentication failed (reject, timeout). Dialog stays showing. - * @param modality sensor modality that triggered the error - * @param failureReason message - */ - void onAuthenticationFailed(@Modality int modality, String failureReason); - - /** - * Authentication rejected, or help message received. - * @param modality sensor modality that triggered the help message - * @param help message - */ - void onHelp(@Modality int modality, String help); - - /** - * Authentication failed. Dialog going away. - * @param modality sensor modality that triggered the error - * @param error message - */ - void onError(@Modality int modality, String error); - - /** UDFPS pointer down event. */ - void onPointerDown(); - - /** - * Get the client's package name - */ - String getOpPackageName(); - - /** - * Get the class name of ConfirmDeviceCredentialActivity. Returns null if the direct caller is - * not ConfirmDeviceCredentialActivity. - */ - String getClassNameIfItIsConfirmDeviceCredentialActivity(); - - /** The requestId of the underlying operation within the framework. */ - long getRequestId(); - - /** - * Animate to credential UI. Typically called after biometric is locked out. - */ - void animateToCredentialUI(boolean isError); - - /** - * @return true if device credential is allowed. - */ - boolean isAllowDeviceCredentials(); - - /** - * Called when the device's orientation changed and the dialog may need to do another - * layout. This is most relevant to UDFPS since configuration changes are not sent by - * the framework in equivalent cases (landscape to reverse landscape) but the dialog - * must remain fixed on the physical sensor location. - */ - void onOrientationChanged(); - - PromptViewModel getViewModel(); -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java index 024c6eaa75bb..31c63d3c57e0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java @@ -16,40 +16,20 @@ package com.android.systemui.biometrics; -import android.annotation.IntDef; import android.annotation.Nullable; +import android.hardware.biometrics.BiometricPrompt; /** * Callback interface for dialog views. These should be implemented by the controller (e.g. * FingerprintDialogImpl) and passed into their views (e.g. FingerprintDialogView). */ public interface AuthDialogCallback { - - int DISMISSED_USER_CANCELED = 1; - int DISMISSED_BUTTON_NEGATIVE = 2; - int DISMISSED_BUTTON_POSITIVE = 3; - int DISMISSED_BIOMETRIC_AUTHENTICATED = 4; - int DISMISSED_ERROR = 5; - int DISMISSED_BY_SYSTEM_SERVER = 6; - int DISMISSED_CREDENTIAL_AUTHENTICATED = 7; - int DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS = 8; - - @IntDef({DISMISSED_USER_CANCELED, - DISMISSED_BUTTON_NEGATIVE, - DISMISSED_BUTTON_POSITIVE, - DISMISSED_BIOMETRIC_AUTHENTICATED, - DISMISSED_ERROR, - DISMISSED_BY_SYSTEM_SERVER, - DISMISSED_CREDENTIAL_AUTHENTICATED, - DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS}) - @interface DismissedReason {} - /** * Invoked when the dialog is dismissed - * @param reason + * @param reason - the {@link BiometricPrompt.DismissedReason} for dismissing * @param credentialAttestation the HAT received from LockSettingsService upon verification */ - void onDismissed(@DismissedReason int reason, + void onDismissed(@BiometricPrompt.DismissedReason int reason, @Nullable byte[] credentialAttestation, long requestId); /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java index 4ac5a12dae03..f7efabac0d14 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java @@ -32,6 +32,7 @@ import android.util.Log; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; import com.android.systemui.statusbar.phone.SystemUIDialog; import javax.inject.Inject; @@ -44,6 +45,7 @@ public class BiometricNotificationDialogFactory { private static final String TAG = "BiometricNotificationDialogFactory"; private final Resources mResources; private final SystemUIDialog.Factory mSystemUIDialogFactory; + private final ShadeDialogContextInteractor mDialogContextInteractor; @Nullable private final FingerprintManager mFingerprintManager; @Nullable private final FaceManager mFaceManager; @@ -51,10 +53,12 @@ public class BiometricNotificationDialogFactory { BiometricNotificationDialogFactory( @Main Resources resources, SystemUIDialog.Factory systemUIDialogFactory, + ShadeDialogContextInteractor shadeDialogContextInteractor, @Nullable FingerprintManager fingerprintManager, @Nullable FaceManager faceManager) { mResources = resources; mSystemUIDialogFactory = systemUIDialogFactory; + mDialogContextInteractor = shadeDialogContextInteractor; mFingerprintManager = fingerprintManager; mFaceManager = faceManager; } @@ -62,7 +66,8 @@ public class BiometricNotificationDialogFactory { Dialog createReenrollDialog( int userId, ActivityStarter activityStarter, BiometricSourceType biometricSourceType, boolean isReenrollForced) { - SystemUIDialog sysuiDialog = mSystemUIDialogFactory.create(); + SystemUIDialog sysuiDialog = + mSystemUIDialogFactory.create(mDialogContextInteractor.getContext()); if (biometricSourceType == BiometricSourceType.FACE) { sysuiDialog.setTitle(mResources.getString(R.string.face_re_enroll_dialog_title)); sysuiDialog.setMessage(mResources.getString(R.string.face_re_enroll_dialog_content)); @@ -90,7 +95,8 @@ public class BiometricNotificationDialogFactory { } private Dialog createReenrollFailureDialog(BiometricSourceType biometricType) { - final SystemUIDialog sysuiDialog = mSystemUIDialogFactory.create(); + final SystemUIDialog sysuiDialog = + mSystemUIDialogFactory.create(mDialogContextInteractor.getContext()); if (biometricType == BiometricSourceType.FACE) { sysuiDialog.setMessage(mResources.getString( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index fd11fa5d3e80..88694ae6db51 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -94,6 +94,7 @@ import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; +import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.VibratorHelper; @@ -656,9 +657,9 @@ public class UdfpsController implements DozeReceiver, Dumpable { } @Inject - public UdfpsController(@NonNull Context context, + public UdfpsController(@NonNull @Main Context context, @NonNull Execution execution, - @NonNull LayoutInflater inflater, + @NonNull @ShadeDisplayAware LayoutInflater inflater, @Nullable FingerprintManager fingerprintManager, @NonNull ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, @NonNull StatusBarStateController statusBarStateController, @@ -676,7 +677,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull KeyguardStateController keyguardStateController, @NonNull DisplayManager displayManager, @Main Handler mainHandler, - @NonNull ConfigurationController configurationController, + @NonNull @Main ConfigurationController configurationController, @NonNull SystemClock systemClock, @NonNull UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, @NonNull SystemUIDialogManager dialogManager, diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index cce1ae1a2947..6473b1c30586 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -237,7 +237,15 @@ constructor( with(mediaHost) { expansion = MediaHostState.EXPANDED expandedMatchesParentHeight = true - showsOnlyActiveMedia = false + if (v2FlagEnabled()) { + // Only show active media to match lock screen, not resumable media, which can + // persist + // for up to 2 days. + showsOnlyActiveMedia = true + } else { + // Maintain old behavior on tablet until V2 flag rolls out. + showsOnlyActiveMedia = false + } falsingProtectionNeeded = false disablePagination = true init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index 3c68e3a09f02..a25faa3a7aec 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -56,6 +56,7 @@ import com.android.systemui.reardisplay.RearDisplayModule; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsImplementation; import com.android.systemui.recents.RecentsModule; +import com.android.systemui.rotationlock.DeviceStateAutoRotateModule; import com.android.systemui.rotationlock.RotationLockModule; import com.android.systemui.rotationlock.RotationLockNewModule; import com.android.systemui.scene.SceneContainerFrameworkModule; @@ -132,6 +133,7 @@ import javax.inject.Named; CollapsedStatusBarFragmentStartableModule.class, ConnectingDisplayViewModel.StartableModule.class, DefaultBlueprintModule.class, + DeviceStateAutoRotateModule.class, EmergencyGestureModule.class, GestureModule.class, HeadsUpModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index fe5a82cb5b8c..f8cf6b007041 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -105,6 +105,7 @@ import com.android.systemui.qs.footer.dagger.FooterActionsModule; import com.android.systemui.recents.Recents; import com.android.systemui.recordissue.RecordIssueModule; import com.android.systemui.retail.RetailModeModule; +import com.android.systemui.rotationlock.DeviceStateAutoRotateModule.BoundsDeviceStateAutoRotateModule; import com.android.systemui.scene.shared.model.SceneContainerConfig; import com.android.systemui.scene.shared.model.SceneDataSource; import com.android.systemui.scene.shared.model.SceneDataSourceDelegator; @@ -145,6 +146,7 @@ import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.ConfigurationControllerModule; import com.android.systemui.statusbar.phone.LetterboxModule; import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule; +import com.android.systemui.statusbar.policy.DeviceStateRotationLockSettingController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.PolicyModule; import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; @@ -391,6 +393,11 @@ public abstract class SystemUIModule { @BindsOptionalOf abstract LockscreenContent optionalLockscreenContent(); + @BindsOptionalOf + @BoundsDeviceStateAutoRotateModule + abstract Optional<DeviceStateRotationLockSettingController> + optionalDeviceStateRotationLockSettingController(); + @SysUISingleton @Binds abstract SystemClock bindSystemClock(SystemClockImpl systemClock); @@ -466,6 +473,16 @@ public abstract class SystemUIModule { return new SceneDataSourceDelegator(applicationScope, config); } + @Provides + @SysUISingleton + static Optional<DeviceStateRotationLockSettingController> + provideDeviceStateRotationLockSettingController( + @BoundsDeviceStateAutoRotateModule + Optional<Optional<DeviceStateRotationLockSettingController>> optionalOfOptional + ) { + return optionalOfOptional.orElseGet(Optional::empty); + } + @Binds abstract SceneDataSource bindSceneDataSource(SceneDataSourceDelegator delegator); diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt index 0908e3b5bf85..a779c4c998a7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt @@ -31,7 +31,6 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION -import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER @@ -67,7 +66,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to System, KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to System, KEY_GESTURE_TYPE_LOCK_SCREEN to System, - KEY_GESTURE_TYPE_OPEN_NOTES to System, KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to System, KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System, @@ -113,7 +111,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) { R.string.shortcut_helper_category_system_controls, KEY_GESTURE_TYPE_LOCK_SCREEN to R.string.shortcut_helper_category_system_controls, KEY_GESTURE_TYPE_ALL_APPS to R.string.shortcut_helper_category_system_controls, - KEY_GESTURE_TYPE_OPEN_NOTES to R.string.shortcut_helper_category_system_apps, KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to R.string.shortcut_helper_category_system_apps, KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.shortcut_helper_category_system_apps, @@ -173,7 +170,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) { R.string.group_system_access_notification_shade, KEY_GESTURE_TYPE_LOCK_SCREEN to R.string.group_system_lock_screen, KEY_GESTURE_TYPE_ALL_APPS to R.string.group_system_access_all_apps_search, - KEY_GESTURE_TYPE_OPEN_NOTES to R.string.group_system_quick_memo, KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to R.string.group_system_access_system_settings, KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.group_system_access_google_assistant, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt index 8bed8537b6c5..a7375f7e7efc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt @@ -178,11 +178,6 @@ constructor(@Main private val resources: Resources, private val inputManager: In private fun systemAppsShortcuts() = listOf( - // Pull up Notes app for quick memo: - // - Meta + Ctrl + N - shortcutInfo(resources.getString(R.string.group_system_quick_memo)) { - command(META_META_ON or META_CTRL_ON, KEYCODE_N) - }, // Access system settings: // - Meta + I shortcutInfo(resources.getString(R.string.group_system_access_system_settings)) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 4ad04bef6836..ef06a85bd0d9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -20,6 +20,10 @@ import android.animation.ValueAnimator import android.util.Log import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -54,6 +58,9 @@ constructor( keyguardOcclusionInteractor: KeyguardOcclusionInteractor, val deviceEntryRepository: DeviceEntryRepository, private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor, + private val communalSettingsInteractor: CommunalSettingsInteractor, + private val communalSceneInteractor: CommunalSceneInteractor, + private val communalInteractor: CommunalInteractor, ) : TransitionInteractor( fromState = KeyguardState.AOD, @@ -103,6 +110,7 @@ constructor( val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val biometricUnlockMode = keyguardInteractor.biometricUnlockState.value.mode val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value + val shouldShowCommunal = communalInteractor.shouldShowCommunal.value if (!maybeHandleInsecurePowerGesture()) { val shouldTransitionToLockscreen = @@ -129,6 +137,9 @@ constructor( (!KeyguardWmStateRefactor.isEnabled && canDismissLockscreen()) || (KeyguardWmStateRefactor.isEnabled && canWakeDirectlyToGone) + val shouldTransitionToCommunal = + communalSettingsInteractor.isV2FlagEnabled() && shouldShowCommunal + if (shouldTransitionToGone) { // TODO(b/360368320): Adapt for scene framework if (SceneContainerFlag.isEnabled) return@collect @@ -137,6 +148,11 @@ constructor( modeOnCanceled = TransitionModeOnCanceled.REVERSE, ownerReason = "canWakeDirectlyToGone = true", ) + } else if (shouldTransitionToCommunal) { + communalSceneInteractor.changeScene( + CommunalScenes.Communal, + "listen for aod to communal", + ) } else if (shouldTransitionToLockscreen) { val modeOnCanceled = if (startedStep.from == KeyguardState.LOCKSCREEN) { 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 6f5f662d6fa3..0700ec639153 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 @@ -159,6 +159,7 @@ constructor( val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value + val canStartDreaming = dreamManager.canStartDreaming(false) if (!deviceEntryInteractor.isLockscreenEnabled()) { if (!SceneContainerFlag.isEnabled) { @@ -191,6 +192,13 @@ constructor( if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } + } else if (canStartDreaming) { + // If we're waking up to dream, transition directly to dreaming without + // showing the lockscreen. + startTransitionTo( + KeyguardState.DREAMING, + ownerReason = "moving from doze to dream", + ) } else { startTransitionTo(KeyguardState.LOCKSCREEN) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 2eeba0f10e0c..3ad862b761fc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -77,7 +77,7 @@ constructor( if (!communalSettingsInteractor.isCommunalFlagEnabled()) { return } - listenForHubToDozing() + listenForHubToAodOrDozing() listenForHubToPrimaryBouncer() listenForHubToAlternateBouncer() listenForHubToOccluded() @@ -123,15 +123,15 @@ constructor( } } - private fun listenForHubToDozing() { + private fun listenForHubToAodOrDozing() { scope.launch { powerInteractor.isAsleep .filterRelevantKeyguardStateAnd { isAsleep -> isAsleep } .collect { - communalSceneInteractor.snapToScene( + communalSceneInteractor.changeScene( newScene = CommunalScenes.Blank, - loggingReason = "hub to dozing", - keyguardState = KeyguardState.DOZING, + loggingReason = "hub to sleep", + keyguardState = keyguardInteractor.asleepKeyguardState.value, ) } } @@ -254,5 +254,6 @@ constructor( val TO_LOCKSCREEN_DURATION = 1.seconds val TO_BOUNCER_DURATION = 400.milliseconds val TO_OCCLUDED_DURATION = 450.milliseconds + val TO_AOD_DURATION = 500.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index da87e38daa9b..c7791cda7046 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -26,6 +26,8 @@ import android.view.HapticFeedbackConstants import android.view.InputDevice import android.view.MotionEvent import android.view.View +import android.view.View.GONE +import android.view.View.INVISIBLE import android.view.View.OnLayoutChangeListener import android.view.View.VISIBLE import android.view.ViewGroup @@ -304,8 +306,9 @@ object KeyguardRootViewBinder { if (isVisible.value) { blueprintViewModel.refreshBlueprint() } - childViews[aodPromotedNotificationId] - ?.setAodNotifIconContainerIsVisible(isVisible) + childViews[aodPromotedNotificationId]?.setAodPromotedNotifIsVisible( + isVisible + ) } } @@ -313,7 +316,7 @@ object KeyguardRootViewBinder { shadeInteractor.isAnyFullyExpanded.collect { isFullyAnyExpanded -> view.visibility = if (isFullyAnyExpanded) { - View.INVISIBLE + INVISIBLE } else { View.VISIBLE } @@ -524,10 +527,10 @@ object KeyguardRootViewBinder { visibility = if (isVisible.value) { alpha = 1f - View.VISIBLE + VISIBLE } else { alpha = 0f - View.INVISIBLE + INVISIBLE } } @@ -541,6 +544,36 @@ object KeyguardRootViewBinder { } } + private fun View.setAodPromotedNotifIsVisible(isVisible: AnimatedValue<Boolean>) { + animate().cancel() + val animatorListener = + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + isVisible.stopAnimating() + } + } + + if (isVisible.isAnimating) { + if (isVisible.value) { + alpha = 0f + visibility = VISIBLE + CrossFadeHelper.fadeIn(this, animatorListener) + } else { + CrossFadeHelper.fadeOut(this, animatorListener) + } + } else { + if (isVisible.value) { + alpha = 1f + visibility = VISIBLE + } else { + // Hide with GONE, not INVISIBLE, so there won't be a redundant bottom + // margin between the smart space and the shelf. + alpha = 0f + visibility = GONE + } + } + } + private fun MotionEvent.isTouchscreenSource(): Boolean { return device?.supportsSource(InputDevice.SOURCE_TOUCHSCREEN) == true } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt index dba2578f79da..c4a7e1ed95e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt @@ -21,6 +21,7 @@ import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransiti import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.AodToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel @@ -33,6 +34,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransiti import com.android.systemui.keyguard.ui.viewmodel.DreamingToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DreamingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel @@ -118,6 +120,12 @@ abstract class DeviceEntryIconTransitionModule { @Binds @IntoSet + abstract fun aodToGlanceableHub( + impl: AodToGlanceableHubTransitionViewModel + ): DeviceEntryIconTransition + + @Binds + @IntoSet abstract fun dozingToGone(impl: DozingToGoneTransitionViewModel): DeviceEntryIconTransition @Binds @@ -258,6 +266,12 @@ abstract class DeviceEntryIconTransitionModule { @Binds @IntoSet + abstract fun glanceableHubToAod( + impl: GlanceableHubToAodTransitionViewModel + ): DeviceEntryIconTransition + + @Binds + @IntoSet abstract fun occludedToGlanceableHub( impl: OccludedToGlanceableHubTransitionViewModel ): DeviceEntryIconTransition diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGlanceableHubTransitionViewModel.kt new file mode 100644 index 000000000000..45f8f10595e4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGlanceableHubTransitionViewModel.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2025 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.keyguard.ui.viewmodel + +import android.util.MathUtils +import com.android.systemui.Flags.lightRevealMigration +import com.android.systemui.communal.ui.compose.TransitionDuration +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.dagger.GlanceableHubBlurComponent +import com.android.systemui.keyguard.shared.model.Edge +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import com.android.systemui.keyguard.ui.transitions.GlanceableHubTransition +import com.android.systemui.scene.shared.model.Scenes +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow + +@SysUISingleton +class AodToGlanceableHubTransitionViewModel +@Inject +constructor( + animationFlow: KeyguardTransitionAnimationFlow, + blurFactory: GlanceableHubBlurComponent.Factory, +) : DeviceEntryIconTransition, GlanceableHubTransition { + private val transitionAnimation = + animationFlow + .setup( + duration = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS.milliseconds, + edge = Edge.create(AOD, Scenes.Communal), + ) + .setupWithoutSceneContainer(edge = Edge.create(AOD, GLANCEABLE_HUB)) + + override val deviceEntryParentViewAlpha: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(1f) + + /** Fade out the lockscreen during a transition to GLANCEABLE_HUB. */ + fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> { + var currentAlpha = 0f + return transitionAnimation.sharedFlow( + duration = 250.milliseconds, + startTime = + if (lightRevealMigration()) { + 100.milliseconds // Wait for the light reveal to "hit" the LS elements. + } else { + 0.milliseconds + }, + onStart = { + currentAlpha = + if (lightRevealMigration()) { + viewState.alpha() + } else { + 0f + } + }, + onStep = { MathUtils.lerp(currentAlpha, 0f, it) }, + ) + } + + override val windowBlurRadius: Flow<Float> = + blurFactory.create(transitionAnimation).getBlurProvider().enterBlurRadius +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt index 75bba489f893..0ccb24a9858a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt @@ -60,6 +60,7 @@ constructor( primaryBouncerToDozingTransitionViewModel: PrimaryBouncerToDozingTransitionViewModel, primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel, + glanceableHubToAodTransitionViewModel: GlanceableHubToAodTransitionViewModel, ) { val color: Flow<Int> = deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground -> @@ -106,6 +107,7 @@ constructor( primaryBouncerToLockscreenTransitionViewModel .deviceEntryBackgroundViewAlpha, lockscreenToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, + glanceableHubToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, ) .merge() .onStart { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt index e6a85c6860c5..9018c58a7e36 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt @@ -39,4 +39,6 @@ constructor(animationFlow: KeyguardTransitionAnimationFlow) { ) val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) + // Notifications should not be shown while transitioning to dream. + val notificationAlpha = transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToAodTransitionViewModel.kt new file mode 100644 index 000000000000..6a45845a02c6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToAodTransitionViewModel.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 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.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor +import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor +import com.android.systemui.keyguard.shared.model.Edge +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import com.android.systemui.scene.shared.model.Scenes +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class GlanceableHubToAodTransitionViewModel +@Inject +constructor( + deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, + animationFlow: KeyguardTransitionAnimationFlow, +) : DeviceEntryIconTransition { + + private val transitionAnimation = + animationFlow + .setup( + duration = FromGlanceableHubTransitionInteractor.TO_AOD_DURATION, + edge = Edge.create(from = Scenes.Communal, to = AOD), + ) + .setupWithoutSceneContainer(edge = Edge.create(KeyguardState.GLANCEABLE_HUB, AOD)) + + val deviceEntryBackgroundViewAlpha: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0f) + + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow<Float> = + transitionAnimation.sharedFlow( + startTime = 233.milliseconds, + duration = 250.milliseconds, + onStep = { it }, + ) + + override val deviceEntryParentViewAlpha: Flow<Float> = + deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfpsEnrolledAndEnabled + -> + if (udfpsEnrolledAndEnabled) { + transitionAnimation.immediatelyTransitionTo(1f) + } else { + emptyFlow() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 8e21745e1a31..def87a8e2717 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -100,6 +100,7 @@ constructor( private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, private val aodToPrimaryBouncerTransitionViewModel: AodToPrimaryBouncerTransitionViewModel, + private val aodToGlanceableHubTransitionViewModel: AodToGlanceableHubTransitionViewModel, private val dozingToDreamingTransitionViewModel: DozingToDreamingTransitionViewModel, private val dozingToGoneTransitionViewModel: DozingToGoneTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, @@ -111,6 +112,7 @@ constructor( private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, private val glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, + private val glanceableHubToAodTransitionViewModel: GlanceableHubToAodTransitionViewModel, private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel, private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel, private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel, @@ -258,6 +260,7 @@ constructor( aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState), aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), aodToPrimaryBouncerTransitionViewModel.lockscreenAlpha, + aodToGlanceableHubTransitionViewModel.lockscreenAlpha(viewState), dozingToDreamingTransitionViewModel.lockscreenAlpha, dozingToGoneTransitionViewModel.lockscreenAlpha(viewState), dozingToLockscreenTransitionViewModel.lockscreenAlpha, @@ -267,6 +270,7 @@ constructor( dreamingToGoneTransitionViewModel.lockscreenAlpha, dreamingToLockscreenTransitionViewModel.lockscreenAlpha, glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, + glanceableHubToAodTransitionViewModel.lockscreenAlpha, goneToAodTransitionViewModel.enterFromTopAnimationAlpha, goneToDozingTransitionViewModel.lockscreenAlpha, goneToDreamingTransitionViewModel.lockscreenAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransition.kt index 21407f3bd6d4..9badf8503c75 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransition.kt @@ -27,6 +27,7 @@ import android.graphics.drawable.RippleDrawable import com.android.internal.R import com.android.internal.annotations.VisibleForTesting import com.android.settingslib.Utils +import com.android.systemui.Flags import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.monet.ColorScheme import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect @@ -51,7 +52,7 @@ interface ColorTransition { open class AnimatingColorTransition( private val defaultColor: Int, private val extractColor: (ColorScheme) -> Int, - private val applyColor: (Int) -> Unit + private val applyColor: (Int) -> Unit, ) : AnimatorUpdateListener, ColorTransition { private val argbEvaluator = ArgbEvaluator() @@ -105,24 +106,60 @@ internal constructor( private val mediaViewHolder: MediaViewHolder, private val multiRippleController: MultiRippleController, private val turbulenceNoiseController: TurbulenceNoiseController, - animatingColorTransitionFactory: AnimatingColorTransitionFactory + animatingColorTransitionFactory: AnimatingColorTransitionFactory, ) { constructor( context: Context, mediaViewHolder: MediaViewHolder, multiRippleController: MultiRippleController, - turbulenceNoiseController: TurbulenceNoiseController + turbulenceNoiseController: TurbulenceNoiseController, ) : this( context, mediaViewHolder, multiRippleController, turbulenceNoiseController, - ::AnimatingColorTransition + ::AnimatingColorTransition, ) + var loadingEffect: LoadingEffect? = null - val bgColor = context.getColor(com.google.android.material.R.color.material_dynamic_neutral20) - val surfaceColor = + // Defaults may be briefly visible before loading a new player's colors + private val backgroundDefault = context.getColor(R.color.system_on_surface_light) + private val primaryDefault = context.getColor(R.color.system_primary_dark) + private val onPrimaryDefault = context.getColor(R.color.system_on_primary_dark) + + private val backgroundColor: AnimatingColorTransition by lazy { + animatingColorTransitionFactory(backgroundDefault, ::backgroundFromScheme) { color -> + mediaViewHolder.albumView.backgroundTintList = ColorStateList.valueOf(color) + } + } + + private val primaryColor: AnimatingColorTransition by lazy { + animatingColorTransitionFactory(primaryDefault, ::primaryFromScheme) { primaryColor -> + val primaryColorList = ColorStateList.valueOf(primaryColor) + mediaViewHolder.actionPlayPause.backgroundTintList = primaryColorList + mediaViewHolder.seamlessButton.backgroundTintList = primaryColorList + (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let { + it.setColor(primaryColorList) + it.effectColor = primaryColorList + } + mediaViewHolder.seekBar.progressBackgroundTintList = primaryColorList + } + } + + private val onPrimaryColor: AnimatingColorTransition by lazy { + animatingColorTransitionFactory(onPrimaryDefault, ::onPrimaryFromScheme) { onPrimaryColor -> + val onPrimaryColorList = ColorStateList.valueOf(onPrimaryColor) + mediaViewHolder.actionPlayPause.imageTintList = onPrimaryColorList + mediaViewHolder.seamlessText.setTextColor(onPrimaryColor) + mediaViewHolder.seamlessIcon.imageTintList = onPrimaryColorList + } + } + + // TODO(media_controls_a11y_colors): remove the below color definitions + private val bgColor = + context.getColor(com.google.android.material.R.color.material_dynamic_neutral20) + private val surfaceColor: AnimatingColorTransition by lazy { animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor -> val colorList = ColorStateList.valueOf(surfaceColor) mediaViewHolder.seamlessIcon.imageTintList = colorList @@ -130,10 +167,12 @@ internal constructor( mediaViewHolder.albumView.backgroundTintList = colorList mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor) } - val accentPrimary = + } + + private val accentPrimary: AnimatingColorTransition by lazy { animatingColorTransitionFactory( loadDefaultColor(R.attr.textColorPrimary), - ::accentPrimaryFromScheme + ::accentPrimaryFromScheme, ) { accentPrimary -> val accentColorList = ColorStateList.valueOf(accentPrimary) mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList @@ -142,11 +181,12 @@ internal constructor( turbulenceNoiseController.updateNoiseColor(accentPrimary) loadingEffect?.updateColor(accentPrimary) } + } - val accentSecondary = + private val accentSecondary: AnimatingColorTransition by lazy { animatingColorTransitionFactory( loadDefaultColor(R.attr.textColorPrimary), - ::accentSecondaryFromScheme + ::accentSecondaryFromScheme, ) { accentSecondary -> val colorList = ColorStateList.valueOf(accentSecondary) (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let { @@ -154,8 +194,9 @@ internal constructor( it.effectColor = colorList } } + } - val colorSeamless = + private val colorSeamless: AnimatingColorTransition by lazy { animatingColorTransitionFactory( loadDefaultColor(R.attr.textColorPrimary), { colorScheme: ColorScheme -> @@ -170,13 +211,14 @@ internal constructor( { seamlessColor: Int -> val accentColorList = ColorStateList.valueOf(seamlessColor) mediaViewHolder.seamlessButton.backgroundTintList = accentColorList - } + }, ) + } - val textPrimary = + private val textPrimary: AnimatingColorTransition by lazy { animatingColorTransitionFactory( loadDefaultColor(R.attr.textColorPrimary), - ::textPrimaryFromScheme + ::textPrimaryFromScheme, ) { textPrimary -> mediaViewHolder.titleText.setTextColor(textPrimary) val textColorList = ColorStateList.valueOf(textPrimary) @@ -189,44 +231,81 @@ internal constructor( } mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary) } + } - val textPrimaryInverse = + private val textPrimaryInverse: AnimatingColorTransition by lazy { animatingColorTransitionFactory( loadDefaultColor(R.attr.textColorPrimaryInverse), - ::textPrimaryInverseFromScheme + ::textPrimaryInverseFromScheme, ) { textPrimaryInverse -> mediaViewHolder.actionPlayPause.imageTintList = ColorStateList.valueOf(textPrimaryInverse) } + } - val textSecondary = + private val textSecondary: AnimatingColorTransition by lazy { animatingColorTransitionFactory( loadDefaultColor(R.attr.textColorSecondary), - ::textSecondaryFromScheme + ::textSecondaryFromScheme, ) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) } + } - val textTertiary = + private val textTertiary: AnimatingColorTransition by lazy { animatingColorTransitionFactory( loadDefaultColor(R.attr.textColorTertiary), - ::textTertiaryFromScheme + ::textTertiaryFromScheme, ) { textTertiary -> mediaViewHolder.seekBar.progressBackgroundTintList = ColorStateList.valueOf(textTertiary) } + } - val colorTransitions = - arrayOf( - surfaceColor, - colorSeamless, - accentPrimary, - accentSecondary, - textPrimary, - textPrimaryInverse, - textSecondary, - textTertiary, - ) + fun getDeviceIconColor(): Int { + if (Flags.mediaControlsA11yColors()) { + return onPrimaryColor.targetColor + } + return surfaceColor.targetColor + } + + fun getAppIconColor(): Int { + if (Flags.mediaControlsA11yColors()) { + return primaryColor.targetColor + } + return accentPrimary.targetColor + } + + fun getSurfaceEffectColor(): Int { + if (Flags.mediaControlsA11yColors()) { + return primaryColor.targetColor + } + return accentPrimary.targetColor + } + + fun getGutsTextColor(): Int { + if (Flags.mediaControlsA11yColors()) { + return context.getColor(com.android.systemui.res.R.color.media_on_background) + } + return textPrimary.targetColor + } + + private fun getColorTransitions(): Array<AnimatingColorTransition> { + return if (Flags.mediaControlsA11yColors()) { + arrayOf(backgroundColor, primaryColor, onPrimaryColor) + } else { + arrayOf( + surfaceColor, + colorSeamless, + accentPrimary, + accentSecondary, + textPrimary, + textPrimaryInverse, + textSecondary, + textTertiary, + ) + } + } private fun loadDefaultColor(id: Int): Int { return Utils.getColorAttr(context, id).defaultColor @@ -234,15 +313,26 @@ internal constructor( fun updateColorScheme(colorScheme: ColorScheme?): Boolean { var anyChanged = false - colorTransitions.forEach { + getColorTransitions().forEach { val isChanged = it.updateColorScheme(colorScheme) // Ignore changes to colorSeamless, since that is expected when toggling dark mode + // TODO(media_controls_a11y_colors): remove, not necessary if (it == colorSeamless) return@forEach anyChanged = isChanged || anyChanged } - colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme } + if (Flags.mediaControlsA11yColors()) { + getSurfaceEffectColor().let { + multiRippleController.updateColor(it) + turbulenceNoiseController.updateNoiseColor(it) + loadingEffect?.updateColor(it) + } + mediaViewHolder.gutsViewHolder.setTextColor(getGutsTextColor()) + colorScheme?.let { mediaViewHolder.gutsViewHolder.setColors(it) } + } else { + colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme } + } return anyChanged } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/MediaColorSchemes.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/MediaColorSchemes.kt index 3c57c83ff9fe..67113a4fe6e7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/MediaColorSchemes.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/MediaColorSchemes.kt @@ -19,28 +19,43 @@ package com.android.systemui.media.controls.ui.animation import com.android.systemui.monet.ColorScheme /** Returns the surface color for media controls based on the scheme. */ +@Deprecated("Remove with media_controls_a11y_colors") internal fun surfaceFromScheme(scheme: ColorScheme) = scheme.accent2.s800 // A2-800 /** Returns the primary accent color for media controls based on the scheme. */ +@Deprecated("Remove with media_controls_a11y_colors") internal fun accentPrimaryFromScheme(scheme: ColorScheme) = scheme.accent1.s100 // A1-100 /** Returns the secondary accent color for media controls based on the scheme. */ +@Deprecated("Remove with media_controls_a11y_colors") internal fun accentSecondaryFromScheme(scheme: ColorScheme) = scheme.accent1.s200 // A1-200 /** Returns the primary text color for media controls based on the scheme. */ +@Deprecated("Remove with media_controls_a11y_colors") internal fun textPrimaryFromScheme(scheme: ColorScheme) = scheme.neutral1.s50 // N1-50 /** Returns the inverse of the primary text color for media controls based on the scheme. */ +@Deprecated("Remove with media_controls_a11y_colors") internal fun textPrimaryInverseFromScheme(scheme: ColorScheme) = scheme.neutral1.s900 // N1-900 /** Returns the secondary text color for media controls based on the scheme. */ +@Deprecated("Remove with media_controls_a11y_colors") internal fun textSecondaryFromScheme(scheme: ColorScheme) = scheme.neutral2.s200 // N2-200 /** Returns the tertiary text color for media controls based on the scheme. */ +@Deprecated("Remove with media_controls_a11y_colors") internal fun textTertiaryFromScheme(scheme: ColorScheme) = scheme.neutral2.s400 // N2-400 /** Returns the color for the start of the background gradient based on the scheme. */ +@Deprecated("Remove with media_controls_a11y_colors") internal fun backgroundStartFromScheme(scheme: ColorScheme) = scheme.accent2.s700 // A2-700 /** Returns the color for the end of the background gradient based on the scheme. */ +@Deprecated("Remove with media_controls_a11y_colors") internal fun backgroundEndFromScheme(scheme: ColorScheme) = scheme.accent1.s700 // A1-700 + +internal fun backgroundFromScheme(scheme: ColorScheme) = scheme.materialScheme.getOnSurface() + +internal fun primaryFromScheme(scheme: ColorScheme) = scheme.materialScheme.getPrimaryFixed() + +internal fun onPrimaryFromScheme(scheme: ColorScheme) = scheme.materialScheme.getOnPrimaryFixed() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt index 910d3a84aeae..3f538203aee9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt @@ -36,6 +36,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch import com.android.settingslib.widget.AdaptiveIcon +import com.android.systemui.Flags import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Background @@ -49,7 +50,9 @@ import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.viewmodel.MediaActionViewModel import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_END_ALPHA +import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_START_ALPHA +import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.SEMANTIC_ACTIONS_ALL import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.SEMANTIC_ACTIONS_COMPACT import com.android.systemui.media.controls.ui.viewmodel.MediaOutputSwitcherViewModel @@ -198,7 +201,9 @@ object MediaControlViewBinder { is Icon.Loaded -> { val icon = viewModel.deviceIcon.drawable if (icon is AdaptiveIcon) { - icon.setBackgroundColor(viewController.colorSchemeTransition.bgColor) + icon.setBackgroundColor( + viewController.colorSchemeTransition.getDeviceIconColor() + ) } viewHolder.seamlessIcon.setImageDrawable(icon) } @@ -431,11 +436,16 @@ object MediaControlViewBinder { TAG, ) val isArtworkBound = wallpaperColors != null + val darkTheme = !Flags.mediaControlsA11yColors() val scheme = - wallpaperColors?.let { ColorScheme(it, true, Style.CONTENT) } + wallpaperColors?.let { ColorScheme(it, darkTheme, Style.CONTENT) } ?: let { if (viewModel.launcherIcon is Icon.Loaded) { - MediaArtworkHelper.getColorScheme(viewModel.launcherIcon.drawable, TAG) + MediaArtworkHelper.getColorScheme( + viewModel.launcherIcon.drawable, + TAG, + darkTheme, + ) } else { null } @@ -496,7 +506,7 @@ object MediaControlViewBinder { } } else { viewHolder.appIcon.setColorFilter( - viewController.colorSchemeTransition.accentPrimary.targetColor + viewController.colorSchemeTransition.getAppIconColor() ) viewHolder.appIcon.setImageIcon(viewModel.appIcon) } @@ -528,12 +538,24 @@ object MediaControlViewBinder { height: Int, ): LayerDrawable { val albumArt = MediaArtworkHelper.getScaledBackground(context, artworkIcon, width, height) + val startAlpha = + if (Flags.mediaControlsA11yColors()) { + MEDIA_PLAYER_SCRIM_START_ALPHA + } else { + MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY + } + val endAlpha = + if (Flags.mediaControlsA11yColors()) { + MEDIA_PLAYER_SCRIM_END_ALPHA + } else { + MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY + } return MediaArtworkHelper.setUpGradientColorOnDrawable( albumArt, context.getDrawable(R.drawable.qs_media_scrim)?.mutate() as GradientDrawable, mutableColorScheme, - MEDIA_PLAYER_SCRIM_START_ALPHA, - MEDIA_PLAYER_SCRIM_END_ALPHA, + startAlpha, + endAlpha, ) } @@ -572,7 +594,7 @@ object MediaControlViewBinder { maxSize, maxSize, button.context.resources.displayMetrics.density, - colorSchemeTransition.accentPrimary.currentColor, + colorSchemeTransition.getSurfaceEffectColor(), opacity = 100, sparkleStrength = 0f, baseRingFadeParams = null, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt index 34f7c4dcaec0..c9716be52408 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt @@ -18,6 +18,9 @@ package com.android.systemui.media.controls.ui.binder import android.animation.Animator import android.animation.ObjectAnimator +import android.icu.text.MeasureFormat +import android.icu.util.Measure +import android.icu.util.MeasureUnit import android.text.format.DateUtils import androidx.annotation.UiThread import androidx.lifecycle.Observer @@ -28,8 +31,11 @@ import com.android.systemui.media.controls.ui.drawable.SquigglyProgress import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.res.R +import java.util.Locale private const val TAG = "SeekBarObserver" +private const val MIN_IN_SEC = 60 +private const val HOUR_IN_SEC = MIN_IN_SEC * 60 /** * Observer for changes from SeekBarViewModel. @@ -127,10 +133,9 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : } holder.seekBar.setMax(data.duration) - val totalTimeString = - DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS) + val totalTimeDescription = formatTimeContentDescription(data.duration) if (data.scrubbing) { - holder.scrubbingTotalTimeView.text = totalTimeString + holder.scrubbingTotalTimeView.text = formatTimeLabel(data.duration) } data.elapsedTime?.let { @@ -148,20 +153,62 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : } } - val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS) + val elapsedTimeDescription = formatTimeContentDescription(it) if (data.scrubbing) { - holder.scrubbingElapsedTimeView.text = elapsedTimeString + holder.scrubbingElapsedTimeView.text = formatTimeLabel(it) } holder.seekBar.contentDescription = holder.seekBar.context.getString( R.string.controls_media_seekbar_description, - elapsedTimeString, - totalTimeString + elapsedTimeDescription, + totalTimeDescription, ) } } + /** Returns a time string suitable for display, e.g. "12:34" */ + private fun formatTimeLabel(milliseconds: Int): CharSequence { + return DateUtils.formatElapsedTime(milliseconds / DateUtils.SECOND_IN_MILLIS) + } + + /** + * Returns a time string suitable for content description, e.g. "12 minutes 34 seconds" + * + * Follows same logic as Chronometer#formatDuration + */ + private fun formatTimeContentDescription(milliseconds: Int): CharSequence { + var seconds = milliseconds / DateUtils.SECOND_IN_MILLIS + + val hours = + if (seconds >= HOUR_IN_SEC) { + seconds / HOUR_IN_SEC + } else { + 0 + } + seconds -= hours * HOUR_IN_SEC + + val minutes = + if (seconds >= MIN_IN_SEC) { + seconds / MIN_IN_SEC + } else { + 0 + } + seconds -= minutes * MIN_IN_SEC + + val measures = arrayListOf<Measure>() + if (hours > 0) { + measures.add(Measure(hours, MeasureUnit.HOUR)) + } + if (minutes > 0) { + measures.add(Measure(minutes, MeasureUnit.MINUTE)) + } + measures.add(Measure(seconds, MeasureUnit.SECOND)) + + return MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures(*measures.toTypedArray()) + } + @VisibleForTesting open fun buildResetAnimator(targetTime: Int): Animator { val animator = @@ -169,7 +216,7 @@ open class SeekBarObserver(private val holder: MediaViewHolder) : holder.seekBar, "progress", holder.seekBar.progress, - targetTime + RESET_ANIMATION_DURATION_MS + targetTime + RESET_ANIMATION_DURATION_MS, ) animator.setAutoCancel(true) animator.duration = RESET_ANIMATION_DURATION_MS.toLong() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt index 606f71a162d8..d26a723035c0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt @@ -55,7 +55,7 @@ constructor( private val bypassController: KeyguardBypassController, private val statusBarStateController: SysuiStatusBarStateController, @ShadeDisplayAware private val context: Context, - configurationController: ConfigurationController, + @ShadeDisplayAware configurationController: ConfigurationController, private val splitShadeStateController: SplitShadeStateController, private val logger: KeyguardMediaControllerLogger, dumpManager: DumpManager, 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 93c4bafe4273..55d8b8c4def0 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 @@ -132,7 +132,7 @@ class MediaCarouselController @Inject constructor( @Application applicationScope: CoroutineScope, - private val context: Context, + @Main private val context: Context, private val mediaControlPanelFactory: Provider<MediaControlPanel>, private val visualStabilityProvider: VisualStabilityProvider, private val mediaHostStatesManager: MediaHostStatesManager, @@ -143,7 +143,7 @@ constructor( @Background private val bgExecutor: Executor, @Background private val backgroundDispatcher: CoroutineDispatcher, private val mediaManager: MediaDataManager, - configurationController: ConfigurationController, + @Main configurationController: ConfigurationController, private val falsingManager: FalsingManager, dumpManager: DumpManager, private val logger: MediaUiEventLogger, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index 2bf6a10c5258..694a4c7e493d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -23,6 +23,10 @@ import static com.android.systemui.Flags.communalHub; import static com.android.systemui.Flags.mediaLockscreenLaunchAnimation; import static com.android.systemui.media.controls.domain.pipeline.MediaActionsKt.getNotificationActions; import static com.android.systemui.media.controls.shared.model.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS; +import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_END_ALPHA; +import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY; +import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_START_ALPHA; +import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY; import android.animation.Animator; import android.animation.AnimatorInflater; @@ -175,9 +179,7 @@ public class MediaControlPanel { protected static final int SMARTSPACE_CARD_DISMISS_EVENT = 761; private static final float REC_MEDIA_COVER_SCALE_FACTOR = 1.25f; - private static final float MEDIA_SCRIM_START_ALPHA = 0.25f; private static final float MEDIA_REC_SCRIM_START_ALPHA = 0.15f; - private static final float MEDIA_PLAYER_SCRIM_END_ALPHA = 1.0f; private static final float MEDIA_REC_SCRIM_END_ALPHA = 1.0f; private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS); @@ -305,7 +307,7 @@ public class MediaControlPanel { */ @Inject public MediaControlPanel( - Context context, + @Main Context context, @Background Executor backgroundExecutor, @Main DelayableExecutor mainExecutor, ActivityStarter activityStarter, @@ -730,7 +732,7 @@ public class MediaControlPanel { Drawable icon = device.getIcon(); if (icon instanceof AdaptiveIcon) { AdaptiveIcon aIcon = (AdaptiveIcon) icon; - aIcon.setBackgroundColor(mColorSchemeTransition.getBgColor()); + aIcon.setBackgroundColor(mColorSchemeTransition.getDeviceIconColor()); iconView.setImageDrawable(aIcon); } else { iconView.setImageDrawable(icon); @@ -921,8 +923,9 @@ public class MediaControlPanel { boolean isArtworkBound; Icon artworkIcon = data.getArtwork(); WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon); + boolean darkTheme = !Flags.mediaControlsA11yColors(); if (wallpaperColors != null) { - mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT); + mutableColorScheme = new ColorScheme(wallpaperColors, darkTheme, Style.CONTENT); artwork = addGradientToPlayerAlbum(artworkIcon, mutableColorScheme, finalWidth, finalHeight); isArtworkBound = true; @@ -933,8 +936,8 @@ public class MediaControlPanel { try { Drawable icon = mContext.getPackageManager() .getApplicationIcon(data.getPackageName()); - mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), true, - Style.CONTENT); + mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), + darkTheme, Style.CONTENT); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e); } @@ -950,7 +953,8 @@ public class MediaControlPanel { mArtworkBoundId = reqId; // Transition Colors to current color scheme - boolean colorSchemeChanged = mColorSchemeTransition.updateColorScheme(colorScheme); + boolean colorSchemeChanged; + colorSchemeChanged = mColorSchemeTransition.updateColorScheme(colorScheme); // Bind the album view to the artwork or a transition drawable ImageView albumView = mMediaViewHolder.getAlbumView(); @@ -973,7 +977,6 @@ public class MediaControlPanel { transitionDrawable.setLayerGravity(0, Gravity.CENTER); transitionDrawable.setLayerGravity(1, Gravity.CENTER); transitionDrawable.setCrossFadeEnabled(true); - albumView.setImageDrawable(transitionDrawable); transitionDrawable.startTransition(isArtworkBound ? 333 : 80); } @@ -986,8 +989,7 @@ public class MediaControlPanel { appIconView.clearColorFilter(); if (data.getAppIcon() != null && !data.getResumption()) { appIconView.setImageIcon(data.getAppIcon()); - appIconView.setColorFilter( - mColorSchemeTransition.getAccentPrimary().getTargetColor()); + appIconView.setColorFilter(mColorSchemeTransition.getAppIconColor()); } else { // Resume players use launcher icon appIconView.setColorFilter(getGrayscaleFilter()); @@ -1092,8 +1094,12 @@ public class MediaControlPanel { Drawable albumArt = getScaledBackground(artworkIcon, width, height); GradientDrawable gradient = (GradientDrawable) mContext.getDrawable( R.drawable.qs_media_scrim).mutate(); + if (Flags.mediaControlsA11yColors()) { + return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme, + MEDIA_PLAYER_SCRIM_START_ALPHA, MEDIA_PLAYER_SCRIM_END_ALPHA); + } return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme, - MEDIA_SCRIM_START_ALPHA, MEDIA_PLAYER_SCRIM_END_ALPHA); + MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY, MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY); } @VisibleForTesting @@ -1113,12 +1119,21 @@ public class MediaControlPanel { private LayerDrawable setupGradientColorOnDrawable(Drawable albumArt, GradientDrawable gradient, ColorScheme mutableColorScheme, float startAlpha, float endAlpha) { + int startColor; + int endColor; + if (Flags.mediaControlsA11yColors()) { + startColor = MediaColorSchemesKt.backgroundFromScheme(mutableColorScheme); + endColor = startColor; + } else { + startColor = MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme); + endColor = MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme); + } gradient.setColors(new int[]{ ColorUtilKt.getColorWithAlpha( - MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme), + startColor, startAlpha), ColorUtilKt.getColorWithAlpha( - MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme), + endColor, endAlpha), }); return new LayerDrawable(new Drawable[]{albumArt, gradient}); @@ -1308,7 +1323,7 @@ public class MediaControlPanel { /* maxWidth= */ maxSize, /* maxHeight= */ maxSize, /* pixelDensity= */ getContext().getResources().getDisplayMetrics().density, - mColorSchemeTransition.getAccentPrimary().getCurrentColor(), + /* color= */ mColorSchemeTransition.getSurfaceEffectColor(), /* opacity= */ 100, /* sparkleStrength= */ 0f, /* baseRingFadeParams= */ null, @@ -1330,10 +1345,13 @@ public class MediaControlPanel { int width = targetView.getWidth(); int height = targetView.getHeight(); Random random = new Random(); + float luminosity = (Flags.mediaControlsA11yColors()) + ? 0.6f + : TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER; return new TurbulenceNoiseAnimationConfig( /* gridCount= */ 2.14f, - TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER, + /* luminosityMultiplier= */ luminosity, /* noiseOffsetX= */ random.nextFloat(), /* noiseOffsetY= */ random.nextFloat(), /* noiseOffsetZ= */ random.nextFloat(), @@ -1341,7 +1359,7 @@ public class MediaControlPanel { /* noiseMoveSpeedY= */ 0f, TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z, // Color will be correctly updated in ColorSchemeTransition. - /* color= */ mColorSchemeTransition.getAccentPrimary().getCurrentColor(), + /* color= */ mColorSchemeTransition.getSurfaceEffectColor(), /* screenColor= */ Color.BLACK, width, height, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt index 20593942148b..b64cb3dd456a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt @@ -113,7 +113,7 @@ constructor( private val dreamOverlayStateController: DreamOverlayStateController, private val keyguardInteractor: KeyguardInteractor, communalTransitionViewModel: CommunalTransitionViewModel, - configurationController: ConfigurationController, + @ShadeDisplayAware configurationController: ConfigurationController, wakefulnessLifecycle: WakefulnessLifecycle, shadeInteractor: ShadeInteractor, private val secureSettings: SecureSettings, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index 2b36872dbe36..b687dce20b06 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -81,8 +81,8 @@ import javax.inject.Inject open class MediaViewController @Inject constructor( - private val context: Context, - private val configurationController: ConfigurationController, + @Main private val context: Context, + @Main private val configurationController: ConfigurationController, private val mediaHostStatesManager: MediaHostStatesManager, private val logger: MediaViewLogger, private val seekBarViewModel: SeekBarViewModel, @@ -1137,6 +1137,7 @@ constructor( ) { gutsViewHolder.gutsText.setTypeface(menuTF) gutsViewHolder.dismissText.setTypeface(menuTF) + gutsViewHolder.cancelText.setTypeface(menuTF) titleText.setTypeface(titleTF) artistText.setTypeface(artistTF) seamlessText.setTypeface(menuTF) @@ -1237,9 +1238,15 @@ constructor( val width = targetView.width val height = targetView.height val random = Random() + val luminosity = + if (Flags.mediaControlsA11yColors()) { + 0.6f + } else { + TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER + } return TurbulenceNoiseAnimationConfig( gridCount = 2.14f, - TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER, + luminosity, random.nextFloat(), random.nextFloat(), random.nextFloat(), @@ -1247,7 +1254,7 @@ constructor( noiseMoveSpeedY = 0f, TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z, // Color will be correctly updated in ColorSchemeTransition. - colorSchemeTransition.accentPrimary.currentColor, + colorSchemeTransition.getSurfaceEffectColor(), screenColor = Color.BLACK, width.toFloat(), height.toFloat(), diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaArtworkHelper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaArtworkHelper.kt index 14a4e2656d7d..557032551308 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaArtworkHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaArtworkHelper.kt @@ -25,7 +25,9 @@ import android.graphics.drawable.GradientDrawable import android.graphics.drawable.Icon import android.graphics.drawable.LayerDrawable import android.util.Log +import com.android.systemui.Flags import com.android.systemui.media.controls.ui.animation.backgroundEndFromScheme +import com.android.systemui.media.controls.ui.animation.backgroundFromScheme import com.android.systemui.media.controls.ui.animation.backgroundStartFromScheme import com.android.systemui.monet.ColorScheme import com.android.systemui.monet.Style @@ -89,22 +91,30 @@ object MediaArtworkHelper { startAlpha: Float, endAlpha: Float, ): LayerDrawable { + val startColor = + if (Flags.mediaControlsA11yColors()) { + backgroundFromScheme(colorScheme) + } else { + backgroundStartFromScheme(colorScheme) + } + val endColor = + if (Flags.mediaControlsA11yColors()) { + startColor + } else { + backgroundEndFromScheme(colorScheme) + } gradient.colors = intArrayOf( - getColorWithAlpha(backgroundStartFromScheme(colorScheme), startAlpha), - getColorWithAlpha(backgroundEndFromScheme(colorScheme), endAlpha), + getColorWithAlpha(startColor, startAlpha), + getColorWithAlpha(endColor, endAlpha), ) return LayerDrawable(arrayOf(albumArt, gradient)) } /** Returns [ColorScheme] of media app given its [icon]. */ - fun getColorScheme( - icon: Drawable, - tag: String, - @Style.Type style: Int = Style.TONAL_SPOT, - ): ColorScheme? { + fun getColorScheme(icon: Drawable, tag: String, darkTheme: Boolean): ColorScheme? { return try { - ColorScheme(WallpaperColors.fromDrawable(icon), true, style) + ColorScheme(WallpaperColors.fromDrawable(icon), darkTheme, Style.CONTENT) } catch (e: PackageManager.NameNotFoundException) { Log.w(tag, "Fail to get media app info", e) null diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/GutsViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/GutsViewHolder.kt index a667c5819062..fa500ae53491 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/GutsViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/GutsViewHolder.kt @@ -22,7 +22,10 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.TextView +import com.android.systemui.Flags import com.android.systemui.media.controls.ui.animation.accentPrimaryFromScheme +import com.android.systemui.media.controls.ui.animation.onPrimaryFromScheme +import com.android.systemui.media.controls.ui.animation.primaryFromScheme import com.android.systemui.media.controls.ui.animation.surfaceFromScheme import com.android.systemui.media.controls.ui.animation.textPrimaryFromScheme import com.android.systemui.monet.ColorScheme @@ -35,7 +38,7 @@ import com.android.systemui.res.R * Both [MediaViewHolder] and [RecommendationViewHolder] use the same guts menu layout, so this * class helps share logic between the two. */ -class GutsViewHolder constructor(itemView: View) { +class GutsViewHolder(itemView: View) { val gutsText: TextView = itemView.requireViewById(R.id.remove_text) val cancel: View = itemView.requireViewById(R.id.cancel) val cancelText: TextView = itemView.requireViewById(R.id.cancel_text) @@ -44,7 +47,9 @@ class GutsViewHolder constructor(itemView: View) { val settings: ImageButton = itemView.requireViewById(R.id.settings) private var isDismissible: Boolean = true + // TODO(media_controls_a11y_colors): make private var colorScheme: ColorScheme? = null + private var textColorFixed: Int? = null /** Marquees the main text of the guts menu. */ fun marquee(start: Boolean, delay: Long, tag: String) { @@ -67,12 +72,43 @@ class GutsViewHolder constructor(itemView: View) { /** Sets the right colors on all the guts views based on the given [ColorScheme]. */ fun setColors(scheme: ColorScheme) { colorScheme = scheme - setSurfaceColor(surfaceFromScheme(scheme)) - setTextPrimaryColor(textPrimaryFromScheme(scheme)) - setAccentPrimaryColor(accentPrimaryFromScheme(scheme)) + + if (Flags.mediaControlsA11yColors()) { + textColorFixed?.let { setTextColor(it) } + setPrimaryColor(primaryFromScheme(scheme)) + setOnPrimaryColor(onPrimaryFromScheme(scheme)) + } else { + setSurfaceColor(surfaceFromScheme(scheme)) + setTextPrimaryColor(textPrimaryFromScheme(scheme)) + setAccentPrimaryColor(accentPrimaryFromScheme(scheme)) + } + } + + private fun setPrimaryColor(color: Int) { + val colorList = ColorStateList.valueOf(color) + dismissText.backgroundTintList = colorList + cancelText.backgroundTintList = colorList + } + + private fun setOnPrimaryColor(color: Int) { + dismissText.setTextColor(color) + if (!isDismissible) { + cancelText.setTextColor(color) + } + } + + fun setTextColor(color: Int) { + textColorFixed = color + gutsText.setTextColor(color) + settings.imageTintList = ColorStateList.valueOf(color) + + if (isDismissible) { + cancelText.setTextColor(color) + } } /** Sets the surface color on all guts views that use it. */ + @Deprecated("Remove with media_controls_a11y_colors") fun setSurfaceColor(surfaceColor: Int) { dismissText.setTextColor(surfaceColor) if (!isDismissible) { @@ -81,6 +117,7 @@ class GutsViewHolder constructor(itemView: View) { } /** Sets the primary accent color on all guts views that use it. */ + @Deprecated("Remove with media_controls_a11y_colors") fun setAccentPrimaryColor(accentPrimary: Int) { val accentColorList = ColorStateList.valueOf(accentPrimary) settings.imageTintList = accentColorList @@ -89,6 +126,7 @@ class GutsViewHolder constructor(itemView: View) { } /** Sets the primary text color on all guts views that use it. */ + @Deprecated("Remove with media_controls_a11y_colors") fun setTextPrimaryColor(textPrimary: Int) { val textColorList = ColorStateList.valueOf(textPrimary) gutsText.setTextColor(textColorList) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt index 61e4d95a88e6..bcda485272c2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt @@ -418,7 +418,11 @@ class MediaControlViewModel( ) const val TURBULENCE_NOISE_PLAY_MS_DURATION = 7500L - const val MEDIA_PLAYER_SCRIM_START_ALPHA = 0.25f - const val MEDIA_PLAYER_SCRIM_END_ALPHA = 1.0f + @Deprecated("Remove with media_controls_a11y_colors flag") + const val MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY = 0.25f + @Deprecated("Remove with media_controls_a11y_colors flag") + const val MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY = 1.0f + const val MEDIA_PLAYER_SCRIM_START_ALPHA = 0.65f + const val MEDIA_PLAYER_SCRIM_END_ALPHA = 0.75f } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt b/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt index 24bb16a11fe7..3a81102699f7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt @@ -27,9 +27,7 @@ object QSEvents { private set fun setLoggerForTesting(): UiEventLoggerFake { - return UiEventLoggerFake().also { - qsUiEventsLogger = it - } + return UiEventLoggerFake().also { qsUiEventsLogger = it } } fun resetLogger() { @@ -40,32 +38,28 @@ object QSEvents { enum class QSEvent(private val _id: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "Tile clicked. It has an instance id and a spec (or packageName)") QS_ACTION_CLICK(387), - - @UiEvent(doc = "Tile secondary button clicked. " + - "It has an instance id and a spec (or packageName)") + @UiEvent( + doc = + "Tile secondary button clicked. " + "It has an instance id and a spec (or packageName)" + ) QS_ACTION_SECONDARY_CLICK(388), - @UiEvent(doc = "Tile long clicked. It has an instance id and a spec (or packageName)") QS_ACTION_LONG_PRESS(389), - - @UiEvent(doc = "Quick Settings panel expanded") - QS_PANEL_EXPANDED(390), - - @UiEvent(doc = "Quick Settings panel collapsed") - QS_PANEL_COLLAPSED(391), - - @UiEvent(doc = "Tile visible in Quick Settings panel. The tile may be in a different page. " + - "It has an instance id and a spec (or packageName)") + @UiEvent(doc = "Quick Settings panel expanded") QS_PANEL_EXPANDED(390), + @UiEvent(doc = "Quick Settings panel collapsed") QS_PANEL_COLLAPSED(391), + @UiEvent( + doc = + "Tile visible in Quick Settings panel. The tile may be in a different page. " + + "It has an instance id and a spec (or packageName)" + ) QS_TILE_VISIBLE(392), - - @UiEvent(doc = "Quick Quick Settings panel expanded") - QQS_PANEL_EXPANDED(393), - - @UiEvent(doc = "Quick Quick Settings panel collapsed") - QQS_PANEL_COLLAPSED(394), - - @UiEvent(doc = "Tile visible in Quick Quick Settings panel. " + - "It has an instance id and a spec (or packageName)") + @UiEvent(doc = "Quick Quick Settings panel expanded") QQS_PANEL_EXPANDED(393), + @UiEvent(doc = "Quick Quick Settings panel collapsed") QQS_PANEL_COLLAPSED(394), + @UiEvent( + doc = + "Tile visible in Quick Quick Settings panel. " + + "It has an instance id and a spec (or packageName)" + ) QQS_TILE_VISIBLE(395); override fun getId() = _id @@ -73,47 +67,32 @@ enum class QSEvent(private val _id: Int) : UiEventLogger.UiEventEnum { enum class QSEditEvent(private val _id: Int) : UiEventLogger.UiEventEnum { - @UiEvent(doc = "Tile removed from current tiles") - QS_EDIT_REMOVE(210), - - @UiEvent(doc = "Tile added to current tiles") - QS_EDIT_ADD(211), - - @UiEvent(doc = "Tile moved") - QS_EDIT_MOVE(212), - - @UiEvent(doc = "QS customizer open") - QS_EDIT_OPEN(213), - - @UiEvent(doc = "QS customizer closed") - QS_EDIT_CLOSED(214), - - @UiEvent(doc = "QS tiles reset") - QS_EDIT_RESET(215); + @UiEvent(doc = "Tile removed from current tiles") QS_EDIT_REMOVE(210), + @UiEvent(doc = "Tile added to current tiles") QS_EDIT_ADD(211), + @UiEvent(doc = "Tile moved") QS_EDIT_MOVE(212), + @UiEvent(doc = "QS customizer open") QS_EDIT_OPEN(213), + @UiEvent(doc = "QS customizer closed") QS_EDIT_CLOSED(214), + @UiEvent(doc = "QS tiles reset") QS_EDIT_RESET(215), + @UiEvent(doc = "QS edit mode resize tile to large") QS_EDIT_RESIZE_LARGE(2122), + @UiEvent(doc = "QS edit mode resize tile to small") QS_EDIT_RESIZE_SMALL(2123); override fun getId() = _id } /** - * Events from the QS DND tile dialog. {@see QSZenModeDialogMetricsLogger} - * Other names for DND (Do Not Disturb) include "Zen" and "Priority mode". + * Events from the QS DND tile dialog. {@see QSZenModeDialogMetricsLogger} Other names for DND (Do + * Not Disturb) include "Zen" and "Priority mode". */ enum class QSDndEvent(private val _id: Int) : UiEventLogger.UiEventEnum { - @UiEvent(doc = "User selected an option on the DND dialog") - QS_DND_CONDITION_SELECT(420), - + @UiEvent(doc = "User selected an option on the DND dialog") QS_DND_CONDITION_SELECT(420), @UiEvent(doc = "User increased countdown duration of DND from the DND dialog") QS_DND_TIME_UP(422), - @UiEvent(doc = "User decreased countdown duration of DND from the DND dialog") QS_DND_TIME_DOWN(423), - @UiEvent(doc = "User enabled DND from the QS DND dialog to last until manually turned off") QS_DND_DIALOG_ENABLE_FOREVER(946), - @UiEvent(doc = "User enabled DND from the QS DND dialog to last until the next alarm goes off") QS_DND_DIALOG_ENABLE_UNTIL_ALARM(947), - @UiEvent(doc = "User enabled DND from the QS DND dialog to last until countdown is done") QS_DND_DIALOG_ENABLE_UNTIL_COUNTDOWN(948); @@ -121,29 +100,17 @@ enum class QSDndEvent(private val _id: Int) : UiEventLogger.UiEventEnum { } enum class QSUserSwitcherEvent(private val _id: Int) : UiEventLogger.UiEventEnum { - @UiEvent(doc = "The current user has been switched in the detail panel") - QS_USER_SWITCH(424), - - @UiEvent(doc = "User switcher QS dialog open") - QS_USER_DETAIL_OPEN(425), - - @UiEvent(doc = "User switcher QS dialog closed") - QS_USER_DETAIL_CLOSE(426), - - @UiEvent(doc = "User switcher QS dialog more settings pressed") - QS_USER_MORE_SETTINGS(427), - - @UiEvent(doc = "The user has added a guest in the detail panel") - QS_USER_GUEST_ADD(754), - + @UiEvent(doc = "The current user has been switched in the detail panel") QS_USER_SWITCH(424), + @UiEvent(doc = "User switcher QS dialog open") QS_USER_DETAIL_OPEN(425), + @UiEvent(doc = "User switcher QS dialog closed") QS_USER_DETAIL_CLOSE(426), + @UiEvent(doc = "User switcher QS dialog more settings pressed") QS_USER_MORE_SETTINGS(427), + @UiEvent(doc = "The user has added a guest in the detail panel") QS_USER_GUEST_ADD(754), @UiEvent(doc = "The user selected 'Start over' after switching to the existing Guest user") QS_USER_GUEST_WIPE(755), - @UiEvent(doc = "The user selected 'Yes, continue' after switching to the existing Guest user") QS_USER_GUEST_CONTINUE(756), - @UiEvent(doc = "The user has pressed 'Remove guest' in the detail panel") QS_USER_GUEST_REMOVE(757); override fun getId() = _id -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index 6ad8bae05d7a..5930a24e01a0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -306,6 +306,7 @@ constructor( sceneState, viewModel.containerViewModel.editModeViewModel.isEditing, snapshotFlow { viewModel.expansionState }.map { it.progress }, + snapshotFlow { viewModel.isQSExpandingOrCollapsing }, ) } @@ -537,6 +538,10 @@ constructor( return qqsVisible.value } + override fun setQSExpandingOrCollapsing(isQSExpandingOrCollapsing: Boolean) { + viewModel.isQSExpandingOrCollapsing = isQSExpandingOrCollapsing + } + private fun setListenerCollections() { lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -877,6 +882,7 @@ private suspend fun synchronizeQsState( state: MutableSceneTransitionLayoutState, editMode: Flow<Boolean>, expansion: Flow<Float>, + isQSExpandingOrCollapsing: Flow<Boolean>, ) { coroutineScope { val animationScope = this @@ -888,31 +894,46 @@ private suspend fun synchronizeQsState( currentTransition = null } - editMode.combine(expansion, ::Pair).collectLatest { (editMode, progress) -> + var lastValidProgress = 0f + combine(editMode, expansion, isQSExpandingOrCollapsing, ::Triple).collectLatest { + (editMode, progress, isQSExpandingOrCollapsing) -> if (editMode && state.currentScene != SceneKeys.EditMode) { state.setTargetScene(SceneKeys.EditMode, animationScope)?.second?.join() } else if (!editMode && state.currentScene == SceneKeys.EditMode) { state.setTargetScene(SceneKeys.QuickSettings, animationScope)?.second?.join() } + if (!editMode) { - when (progress) { - 0f -> snapTo(QuickQuickSettings) - 1f -> snapTo(QuickSettings) - else -> { - val transition = currentTransition - if (transition != null) { - transition.progress = progress - return@collectLatest - } + if (!isQSExpandingOrCollapsing) { + if (progress == 0f) { + snapTo(QuickQuickSettings) + return@collectLatest + } - val newTransition = - ExpansionTransition(progress).also { currentTransition = it } - state.startTransitionImmediately( - animationScope = animationScope, - transition = newTransition, - ) + if (progress == 1f) { + snapTo(QuickSettings) + return@collectLatest } } + + var progress = progress + if (progress >= 0f || progress <= 1f) { + lastValidProgress = progress + } else { + progress = lastValidProgress + } + + val transition = currentTransition + if (transition != null) { + transition.progress = progress + return@collectLatest + } + + val newTransition = ExpansionTransition(progress).also { currentTransition = it } + state.startTransitionImmediately( + animationScope = animationScope, + transition = newTransition, + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index c09d319f82f5..b829bbce2f18 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -35,7 +35,6 @@ import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -43,7 +42,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS import com.android.systemui.media.controls.ui.view.MediaHost @@ -101,7 +99,7 @@ constructor( private val footerActionsController: FooterActionsController, private val sysuiStatusBarStateController: SysuiStatusBarStateController, deviceEntryInteractor: DeviceEntryInteractor, - DisableFlagsInteractor: DisableFlagsInteractor, + disableFlagsInteractor: DisableFlagsInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator, shadeInteractor: ShadeInteractor, @@ -119,7 +117,7 @@ constructor( @Assisted private val lifecycleScope: LifecycleCoroutineScope, ) : Dumpable, ExclusiveActivatable() { - val containerViewModel = containerViewModelFactory.create(true) + val containerViewModel = containerViewModelFactory.create(supportsBrightnessMirroring = true) val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create() private val qqsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QQS) @@ -199,8 +197,8 @@ constructor( val isQsEnabled by hydrator.hydratedStateOf( traceName = "isQsEnabled", - initialValue = DisableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled(), - source = DisableFlagsInteractor.disableFlags.map { it.isQuickSettingsEnabled() }, + initialValue = disableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled(), + source = disableFlagsInteractor.disableFlags.map { it.isQuickSettingsEnabled() }, ) var isInSplitShade by mutableStateOf(false) @@ -308,6 +306,8 @@ constructor( val animateTilesExpansion: Boolean get() = inFirstPage && !mediaSuddenlyAppearingInLandscape + var isQSExpandingOrCollapsing by mutableStateOf(false) + private val inFirstPage: Boolean get() = inFirstPageViewModel.inFirstPage @@ -490,12 +490,12 @@ constructor( qqsMediaHost.apply { expansion = qqsMediaExpansion showsOnlyActiveMedia = true - init(MediaHierarchyManager.LOCATION_QQS) + init(LOCATION_QQS) } qsMediaHost.apply { expansion = MediaHostState.EXPANDED showsOnlyActiveMedia = false - init(MediaHierarchyManager.LOCATION_QS) + init(LOCATION_QS) } } @@ -541,6 +541,7 @@ constructor( println("proposedTranslation", proposedTranslation) println("expansionState", expansionState) println("forceQS", forceQs) + println("isShadeExpandingOrCollapsing", isQSExpandingOrCollapsing) printSection("Derived values") { println("headerTranslation", headerTranslation) println("translationScaleY", translationScaleY) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt index 482cd4014acf..3f279b0f7a74 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt @@ -16,15 +16,18 @@ package com.android.systemui.qs.panels.domain.interactor +import com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel +import com.android.systemui.qs.QSEditEvent import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository import com.android.systemui.qs.panels.data.repository.LargeTileSpanRepository import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.metricSpec import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -40,6 +43,7 @@ constructor( private val repo: DefaultLargeTilesRepository, private val currentTilesInteractor: CurrentTilesInteractor, private val preferencesInteractor: QSPreferencesInteractor, + private val uiEventLogger: UiEventLogger, largeTilesSpanRepo: LargeTileSpanRepository, @PanelsLog private val logBuffer: LogBuffer, @Application private val applicationScope: CoroutineScope, @@ -70,8 +74,18 @@ constructor( val isIcon = !largeTilesSpecs.value.contains(spec) if (toIcon && !isIcon) { preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value - spec) + uiEventLogger.log( + /* event= */ QSEditEvent.QS_EDIT_RESIZE_SMALL, + /* uid= */ 0, + /* packageName= */ spec.metricSpec, + ) } else if (!toIcon && isIcon) { preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value + spec) + uiEventLogger.log( + /* event= */ QSEditEvent.QS_EDIT_RESIZE_LARGE, + /* uid= */ 0, + /* packageName= */ spec.metricSpec, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt index 302242ca11dd..eb6f97942ec0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt @@ -19,8 +19,6 @@ package com.android.systemui.qs.panels.ui.compose import androidx.compose.runtime.Stable import com.android.compose.animation.Bounceable import com.android.systemui.qs.panels.shared.model.SizedTile -import com.android.systemui.qs.panels.ui.model.GridCell -import com.android.systemui.qs.panels.ui.model.TileGridCell import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel @@ -32,18 +30,6 @@ data class BounceableInfo( val bounceEnd: Boolean, ) -fun List<Pair<GridCell, BounceableTileViewModel>>.bounceableInfo( - index: Int, - columns: Int, -): BounceableInfo { - val cell = this[index].first as TileGridCell - // Only look for neighbor bounceables if they are on the same row - val onLastColumn = cell.onLastColumn(cell.column, columns) - val previousTile = getOrNull(index - 1)?.takeIf { cell.column != 0 } - val nextTile = getOrNull(index + 1)?.takeIf { !onLastColumn } - return BounceableInfo(this[index].second, previousTile?.second, nextTile?.second, !onLastColumn) -} - fun List<BounceableTileViewModel>.bounceableInfo( sizedTile: SizedTile<TileViewModel>, index: Int, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt index 1a7ef62a2e7c..a07120629d2b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt @@ -144,12 +144,17 @@ private fun FooterBar( pagerState: PagerState, editButtonViewModelFactory: EditModeButtonViewModel.Factory, ) { + val editButtonViewModel = + rememberViewModel(traceName = "PaginatedGridLayout-editButtonViewModel") { + editButtonViewModelFactory.create() + } + // Use requiredHeight so it won't be squished if the view doesn't quite fit. As this is // expected to be inside a scrollable container, this should not be an issue. // Also, we construct the layout this way to do the following: // * PagerDots is centered in the row, taking as much space as it needs. // * On the start side, we place the BuildNumber, taking as much space as it needs, but - // constrained by the available space left over after PagerDots + // constrained by the available space left over after PagerDots. // * On the end side, we place the edit mode button, with the same constraints as for // BuildNumber (but it will usually fit, as it's just a square button). Row( @@ -178,7 +183,7 @@ private fun FooterBar( ) Row(Modifier.weight(1f)) { Spacer(modifier = Modifier.weight(1f)) - EditModeButton(viewModelFactory = editButtonViewModelFactory) + EditModeButton(viewModel = editButtonViewModel) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index ebfe101948c2..ddadb8879f07 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -22,7 +22,6 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -34,7 +33,6 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.clipScrollableContainer import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -47,28 +45,27 @@ 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.systemGestureExclusion import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Clear -import androidx.compose.material.icons.filled.Remove import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -91,12 +88,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned @@ -113,24 +107,17 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.unit.toSize import androidx.compose.ui.util.fastMap -import androidx.compose.ui.zIndex -import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.compose.animation.bounceable import com.android.compose.modifiers.height import com.android.systemui.common.ui.compose.load import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.SizedTileImpl -import com.android.systemui.qs.panels.ui.compose.BounceableInfo import com.android.systemui.qs.panels.ui.compose.DragAndDropState import com.android.systemui.qs.panels.ui.compose.DragType import com.android.systemui.qs.panels.ui.compose.EditTileListState -import com.android.systemui.qs.panels.ui.compose.bounceableInfo import com.android.systemui.qs.panels.ui.compose.dragAndDropRemoveZone import com.android.systemui.qs.panels.ui.compose.dragAndDropTileList import com.android.systemui.qs.panels.ui.compose.dragAndDropTileSource @@ -142,13 +129,14 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaul import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.AUTO_SCROLL_SPEED import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.AvailableTilesGridMinHeight import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.CurrentTilesGridPadding -import com.android.systemui.qs.panels.ui.compose.infinitegrid.EditModeTileDefaults.TileBadgeSize +import com.android.systemui.qs.panels.ui.compose.selection.InteractiveTileContainer import com.android.systemui.qs.panels.ui.compose.selection.MutableSelectionState -import com.android.systemui.qs.panels.ui.compose.selection.ResizableTileContainer import com.android.systemui.qs.panels.ui.compose.selection.ResizingState import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.FinalResizeOperation import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.TemporaryResizeOperation +import com.android.systemui.qs.panels.ui.compose.selection.StaticTileBadge +import com.android.systemui.qs.panels.ui.compose.selection.TileState import com.android.systemui.qs.panels.ui.compose.selection.rememberResizingState import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState import com.android.systemui.qs.panels.ui.compose.selection.selectableTile @@ -252,13 +240,15 @@ fun DefaultEditTileGrid( AnimatedContent( targetState = listState.dragInProgress || selectionState.selected, label = "QSEditHeader", + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth().heightIn(min = 80.dp), ) { showRemoveTarget -> - EditGridHeader(Modifier.padding(bottom = 26.dp)) { + EditGridHeader { if (showRemoveTarget) { RemoveTileTarget { selectionState.selection?.let { selectionState.unSelect() - onRemoveTile(it.tileSpec) + onRemoveTile(it) } } } else { @@ -380,7 +370,7 @@ private fun RemoveTileTarget(onClick: () -> Unit) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = tileHorizontalArrangement(), modifier = - Modifier.fillMaxHeight() + Modifier.wrapContentSize() .clickable(onClick = onClick) .border(1.dp, LocalContentColor.current, shape = CircleShape) .padding(10.dp), @@ -430,22 +420,15 @@ private fun CurrentTilesGrid( ) .dragAndDropTileList(gridState, { gridContentOffset }, listState) { spec -> onSetTiles(currentListState.tileSpecs()) - selectionState.select(spec, manual = false) + selectionState.select(spec) } .onGloballyPositioned { coordinates -> gridContentOffset = coordinates.positionInRoot() } .testTag(CURRENT_TILES_GRID_TEST_TAG), ) { - EditTiles( - cells, - columns, - listState, - selectionState, - coroutineScope, - largeTilesSpan, - onRemoveTile, - ) { resizingOperation -> + EditTiles(cells, listState, selectionState, coroutineScope, largeTilesSpan, onRemoveTile) { + resizingOperation -> when (resizingOperation) { is TemporaryResizeOperation -> { currentListState.resizeTile(resizingOperation.spec, resizingOperation.toIcon) @@ -453,10 +436,6 @@ private fun CurrentTilesGrid( is FinalResizeOperation -> { // Commit the new size of the tile onResize(resizingOperation.spec, resizingOperation.toIcon) - - // Mark the selection as automatic in case the tile ends up moving to a - // different row with its new size. - selectionState.select(resizingOperation.spec, manual = false) } } } @@ -536,7 +515,6 @@ private fun GridCell.key(index: Int): Any { * Adds a list of [GridCell] to the lazy grid * * @param cells the pairs of [GridCell] to [BounceableTileViewModel] - * @param columns the number of columns of this tile grid * @param dragAndDropState the [DragAndDropState] for this grid * @param selectionState the [MutableSelectionState] for this grid * @param coroutineScope the [CoroutineScope] to be used for the tiles @@ -545,7 +523,6 @@ private fun GridCell.key(index: Int): Any { */ fun LazyGridScope.EditTiles( cells: List<Pair<GridCell, BounceableTileViewModel>>, - columns: Int, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, coroutineScope: CoroutineScope, @@ -581,7 +558,6 @@ fun LazyGridScope.EditTiles( onResize = onResize, onRemoveTile = onRemoveTile, coroutineScope = coroutineScope, - bounceableInfo = cells.bounceableInfo(index, columns), largeTilesSpan = largeTilesSpan, modifier = Modifier.animateItem(), ) @@ -601,83 +577,84 @@ private fun TileGridCell( onRemoveTile: (TileSpec) -> Unit, coroutineScope: CoroutineScope, largeTilesSpan: Int, - bounceableInfo: BounceableInfo, modifier: Modifier = Modifier, ) { val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1) - var selected by remember { mutableStateOf(false) } - val showRemovalBadge = - !selected && cell.tile.availableEditActions.contains(AvailableEditActions.REMOVE) - val selectionAlpha by - animateFloatAsState( - targetValue = if (selected) 1f else 0f, - label = "QSEditTileSelectionAlpha", - ) - val selectionColor = MaterialTheme.colorScheme.primary - val colors = EditModeTileDefaults.editTileColors() - val currentBounceableInfo by rememberUpdatedState(bounceableInfo) - - LaunchedEffect(selectionState.selection?.tileSpec) { - selectionState.selection?.let { - // A delay is introduced on automatic selections such as dragged tiles or reflow caused - // by resizing. This avoids clipping issues on the border and resizing handle, as well - // as letting the selection animation play correctly. - if (!it.manual) { - delay(250) + val canShowRemovalBadge = cell.tile.availableEditActions.contains(AvailableEditActions.REMOVE) + var tileState by remember { mutableStateOf(TileState.None) } + + LaunchedEffect(selectionState.selection, canShowRemovalBadge) { + tileState = + when { + selectionState.selection == cell.tile.tileSpec -> { + if (tileState == TileState.None && canShowRemovalBadge) { + // The tile decoration is None if a tile is newly composed OR the removal + // badge can't be shown. + // For newly composed and selected tiles, such as dragged tiles or moved + // tiles from resizing, introduce a short delay. This avoids clipping issues + // on the border and resizing handle, as well as letting the selection + // animation play correctly. + delay(250) + } + TileState.Selected + } + canShowRemovalBadge -> TileState.Removable + else -> TileState.None } - } - selected = selectionState.selection?.tileSpec == cell.tile.tileSpec } - val state = rememberResizingState(cell.tile.tileSpec, cell.isIcon) - + val resizingState = rememberResizingState(cell.tile.tileSpec, cell.isIcon) val progress: () -> Float = { - if (selected) { - // If selected, return the manual progress from the drag - state.progress() + if (tileState == TileState.Selected) { + resizingState.progress() } else { - // Else, return the target progress for the tile format if (cell.isIcon) 0f else 1f } } - if (!selected) { + if (tileState != TileState.Selected) { // Update the draggable anchor state when the tile's size is not manually toggled - LaunchedEffect(cell.isIcon) { state.updateCurrentValue(cell.isIcon) } + LaunchedEffect(cell.isIcon) { resizingState.updateCurrentValue(cell.isIcon) } } else { // If the tile is selected, listen to new target values from the draggable anchor to toggle // the tile's size - LaunchedEffect(state.temporaryResizeOperation) { onResize(state.temporaryResizeOperation) } - LaunchedEffect(state.finalResizeOperation) { onResize(state.finalResizeOperation) } + LaunchedEffect(resizingState.temporaryResizeOperation) { + onResize(resizingState.temporaryResizeOperation) + } + LaunchedEffect(resizingState.finalResizeOperation) { + onResize(resizingState.finalResizeOperation) + } } val totalPadding = with(LocalDensity.current) { (largeTilesSpan - 1) * TileArrangementPadding.roundToPx() } - - ResizableTileContainer( - selected = selected, - state = state, - selectionAlpha = { selectionAlpha }, - selectionColor = selectionColor, + val colors = EditModeTileDefaults.editTileColors() + val toggleSizeLabel = stringResource(R.string.accessibility_qs_edit_toggle_tile_size_action) + val clickLabel = + when (tileState) { + TileState.None -> null + TileState.Removable -> + stringResource(id = R.string.accessibility_qs_edit_remove_tile_action) + TileState.Selected -> toggleSizeLabel + } + InteractiveTileContainer( + tileState = tileState, + resizingState = resizingState, modifier = - modifier - .height(TileHeight) - .fillMaxWidth() - .onSizeChanged { - // Grab the size before the bounceable to get the idle width - val min = - if (cell.isIcon) it.width else (it.width - totalPadding) / largeTilesSpan - val max = - if (cell.isIcon) (it.width * largeTilesSpan) + totalPadding else it.width - state.updateAnchors(min.toFloat(), max.toFloat()) - } - .bounceable( - bounceable = currentBounceableInfo.bounceable, - previousBounceable = currentBounceableInfo.previousTile, - nextBounceable = currentBounceableInfo.nextTile, - orientation = Orientation.Horizontal, - bounceEnd = currentBounceableInfo.bounceEnd, - ), + modifier.height(TileHeight).fillMaxWidth().onSizeChanged { + // Calculate the min/max width from the idle size + val min = if (cell.isIcon) it.width else (it.width - totalPadding) / largeTilesSpan + val max = if (cell.isIcon) (it.width * largeTilesSpan) + totalPadding else it.width + resizingState.updateAnchors(min.toFloat(), max.toFloat()) + }, + onClick = { + if (tileState == TileState.Removable) { + onRemoveTile(cell.tile.tileSpec) + } else if (tileState == TileState.Selected) { + coroutineScope.launch { resizingState.toggleCurrentValue() } + } + }, + onClickLabel = clickLabel, ) { Box( modifier @@ -688,15 +665,13 @@ private fun TileGridCell( customActions = listOf( // TODO(b/367748260): Add final accessibility actions - CustomAccessibilityAction("Toggle size") { + CustomAccessibilityAction(toggleSizeLabel) { onResize(FinalResizeOperation(cell.tile.tileSpec, !cell.isIcon)) true } ) } - .selectableTile(cell.tile.tileSpec, selectionState) { - coroutineScope.launch { currentBounceableInfo.bounceable.animateBounce() } - } + .selectableTile(cell.tile.tileSpec, selectionState) .dragAndDropTileSource( SizedTileImpl(cell.tile, cell.width), dragAndDropState, @@ -705,16 +680,7 @@ private fun TileGridCell( ) .tileBackground(colors.background) ) { - EditTile(tile = cell.tile, state = state, progress = progress) - } - - if (showRemovalBadge) { - TileBadge( - icon = Icons.Default.Remove, - contentDescription = stringResource(R.string.qs_customize_remove), - ) { - onRemoveTile(cell.tile.tileSpec) - } + EditTile(tile = cell.tile, state = resizingState, progress = progress) } } } @@ -733,7 +699,7 @@ private fun AvailableTileGridCell( val colors = EditModeTileDefaults.editTileColors() val onClick = { onAddTile(cell.tile.tileSpec) - selectionState.select(cell.tile.tileSpec, manual = false) + selectionState.select(cell.tile.tileSpec) } // Displays the tile as an icon tile with the label underneath @@ -742,10 +708,9 @@ private fun AvailableTileGridCell( verticalArrangement = spacedBy(CommonTileDefaults.TilePadding, Alignment.Top), modifier = modifier, ) { - Box { + Box(Modifier.fillMaxWidth().height(TileHeight)) { Box( - Modifier.fillMaxWidth() - .height(TileHeight) + Modifier.fillMaxSize() .clickable(onClick = onClick, onClickLabel = onClickActionName) .semantics(mergeDescendants = true) { this.stateDescription = stateDescription } .dragAndDropTileSource( @@ -756,7 +721,6 @@ private fun AvailableTileGridCell( selectionState.unSelect() } .tileBackground(colors.background) - .tilePadding() ) { // Icon SmallTileContent( @@ -767,7 +731,7 @@ private fun AvailableTileGridCell( ) } - TileBadge( + StaticTileBadge( icon = Icons.Default.Add, contentDescription = onClickActionName, onClick = onClick, @@ -787,39 +751,6 @@ private fun AvailableTileGridCell( } @Composable -private fun TileBadge(icon: ImageVector, contentDescription: String?, onClick: () -> Unit) { - // Use a higher zIndex than the tile to draw over it, and manually create the touch target as - // we're drawing over neighbor tiles as well. - val minTouchTargetSize = LocalMinimumInteractiveComponentSize.current - - Box( - Modifier.zIndex(2f) - .layout { measurable, constraints -> - val size = minTouchTargetSize.roundToPx() - val placeable = measurable.measure(Constraints(size)) - layout(placeable.width, placeable.height) { - val iconRadius = TileBadgeSize.roundToPx() / 2 - val x = constraints.maxWidth - placeable.width / 2 - iconRadius - val y = 0 - placeable.height / 2 + iconRadius - placeable.place(x, y) - } - } - .systemGestureExclusion { Rect(Offset.Zero, it.size.toSize()) } - .pointerInput(Unit) { detectTapGestures { onClick() } } - ) { - val secondaryColor = MaterialTheme.colorScheme.secondary - Icon( - icon, - contentDescription = contentDescription, - modifier = - Modifier.size(TileBadgeSize).align(Alignment.Center).drawBehind { - drawCircle(secondaryColor) - }, - ) - } -} - -@Composable private fun SpacerGridCell(modifier: Modifier = Modifier) { // By default, spacers are invisible and exist purely to catch drag movements Box(modifier.height(TileHeight).fillMaxWidth()) @@ -902,9 +833,8 @@ private object EditModeTileDefaults { const val PLACEHOLDER_ALPHA = .3f const val AUTO_SCROLL_DISTANCE = 100 const val AUTO_SCROLL_SPEED = 2 // 2ms per pixel - val CurrentTilesGridPadding = 8.dp + val CurrentTilesGridPadding = 10.dp val AvailableTilesGridMinHeight = 200.dp - val TileBadgeSize = 20.dp @Composable fun editTileColors(): TileColors = diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt index 26dfc7224ff9..3dfde86bf8d9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt @@ -32,23 +32,17 @@ fun rememberSelectionState(): MutableSelectionState { return remember { MutableSelectionState() } } -/** - * Holds the selected [TileSpec] and whether the selection was manual, i.e. caused by a tap from the - * user. - */ -data class Selection(val tileSpec: TileSpec, val manual: Boolean) - /** Holds the state of the current selection. */ class MutableSelectionState { - /** The [Selection] if a tile is selected, null if not. */ - var selection by mutableStateOf<Selection?>(null) + /** The [TileSpec] of a tile is selected, null if not. */ + var selection by mutableStateOf<TileSpec?>(null) private set val selected: Boolean get() = selection != null - fun select(tileSpec: TileSpec, manual: Boolean) { - selection = Selection(tileSpec, manual) + fun select(tileSpec: TileSpec) { + selection = tileSpec } fun unSelect() { @@ -68,10 +62,10 @@ fun Modifier.selectableTile( return pointerInput(Unit) { detectTapGestures( onTap = { - if (selectionState.selection?.tileSpec == tileSpec) { + if (selectionState.selection == tileSpec) { selectionState.unSelect() } else { - selectionState.select(tileSpec, manual = true) + selectionState.select(tileSpec) } onClick() } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt index 7c472638da63..699e5f6b77e9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt @@ -16,141 +16,232 @@ package com.android.systemui.qs.panels.ui.compose.selection -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring -import androidx.compose.foundation.Canvas +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateOffset +import androidx.compose.animation.core.animateSize +import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.systemGestureExclusion +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.Icon import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import androidx.compose.ui.zIndex +import com.android.compose.modifiers.size import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius -import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingDotSize +import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BADGE_ANGLE_RAD +import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeSize +import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeXOffset +import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeYOffset +import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.RESIZING_PILL_ANGLE_RAD +import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingPillHeight +import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingPillWidth import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.SelectedBorderWidth +import com.android.systemui.qs.panels.ui.compose.selection.TileState.None +import com.android.systemui.qs.panels.ui.compose.selection.TileState.Removable +import com.android.systemui.qs.panels.ui.compose.selection.TileState.Selected +import kotlin.math.cos import kotlin.math.roundToInt -import kotlinx.coroutines.launch +import kotlin.math.sin /** - * Places a dot to handle resizing drag events. Use this on tiles to resize. + * Draws a tile decoration and handles click and drag events for them. * - * The dot is placed vertically centered on the right border. The [content] will have a border when - * selected. + * In states: + * - [TileState.Removable]: removal icon shown in the top end + * - [TileState.Selected]: pill shaped handle shown on the end border, as well as a colored border + * around the content. + * - [TileState.None]: nothing * - * @param selected whether resizing drag events should be handled - * @param state the [ResizingState] for the tile - * @param selectionAlpha the animated value for the dot and border alpha - * @param selectionColor the [Color] of the dot and border + * @param tileState the state for the tile decoration + * @param resizingState the [ResizingState] for the tile + * @param onClick the callback when the tile decoration is clicked */ @Composable -fun ResizableTileContainer( - selected: Boolean, - state: ResizingState, - selectionAlpha: () -> Float, - selectionColor: Color, +fun InteractiveTileContainer( + tileState: TileState, + resizingState: ResizingState, modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + onClickLabel: String? = null, content: @Composable BoxScope.() -> Unit = {}, ) { - Box(modifier.resizable(selected, state).selectionBorder(selectionColor, selectionAlpha)) { - content() - ResizingHandle( - enabled = selected, - state = state, - modifier = - // Higher zIndex to make sure the handle is drawn above the content - Modifier.zIndex(if (selected) 2f else 1f), - ) - } -} + val transition: Transition<TileState> = updateTransition(tileState) + val decorationColor by transition.animateColor() + val decorationAngle by transition.animateAngle() + val decorationSize by transition.animateSize() + val decorationOffset by transition.animateOffset() + val decorationAlpha by transition.animateFloat { state -> if (state == None) 0f else 1f } + val badgeIconAlpha by transition.animateFloat { state -> if (state == Removable) 1f else 0f } + val selectionBorderAlpha by + transition.animateFloat { state -> if (state == Selected) 1f else 0f } -@Composable -private fun ResizingHandle(enabled: Boolean, state: ResizingState, modifier: Modifier = Modifier) { - // Manually creating the touch target around the resizing dot to ensure that the next tile - // does not receive the touch input accidentally. - val minTouchTargetSize = LocalMinimumInteractiveComponentSize.current - val scope = rememberCoroutineScope() Box( - modifier - .layout { measurable, constraints -> - val size = minTouchTargetSize.roundToPx() - val placeable = measurable.measure(Constraints(size, size, size, size)) - layout(placeable.width, placeable.height) { - placeable.place( - x = constraints.maxWidth - placeable.width / 2, - y = constraints.maxHeight / 2 - placeable.height / 2, - ) - } - } - .systemGestureExclusion { Rect(Offset.Zero, it.size.toSize()) } - .anchoredDraggable( - enabled = enabled, - state = state.anchoredDraggableState, - orientation = Orientation.Horizontal, - ) - .clickable(enabled = enabled, interactionSource = null, indication = null) { - scope.launch { state.toggleCurrentValue() } - } + modifier.resizable(tileState == Selected, resizingState).selectionBorder( + MaterialTheme.colorScheme.primary, + SelectedBorderWidth, + ) { + selectionBorderAlpha + } ) { - ResizingDot(enabled = enabled, modifier = Modifier.align(Alignment.Center)) - } -} + content() -@Composable -private fun ResizingDot( - enabled: Boolean, - modifier: Modifier = Modifier, - color: Color = MaterialTheme.colorScheme.primary, -) { - val alpha by animateFloatAsState(if (enabled) 1f else 0f) - val radius by - animateDpAsState( - if (enabled) ResizingDotSize / 2 else 0.dp, - animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), - ) - Canvas(modifier = modifier.size(ResizingDotSize)) { - drawCircle(color = color, radius = radius.toPx(), alpha = alpha) + MinimumInteractiveSizeComponent( + angle = { decorationAngle }, + offset = { decorationOffset }, + ) { + Box( + Modifier.fillMaxSize() + .drawBehind { + drawRoundRect( + color = decorationColor, + topLeft = center - decorationSize.center, + size = decorationSize, + cornerRadius = CornerRadius(size.width / 2), + ) + } + .graphicsLayer { this.alpha = decorationAlpha } + .anchoredDraggable( + enabled = tileState == Selected, + state = resizingState.anchoredDraggableState, + orientation = Orientation.Horizontal, + ) + .clickable( + enabled = tileState != None, + interactionSource = null, + indication = null, + onClickLabel = onClickLabel, + onClick = onClick, + ) + ) { + Icon( + Icons.Default.Remove, + contentDescription = null, + modifier = + Modifier.size( + width = { decorationSize.width.roundToInt() }, + height = { decorationSize.height.roundToInt() }, + ) + .align(Alignment.Center) + .graphicsLayer { this.alpha = badgeIconAlpha }, + ) + } + } } } private fun Modifier.selectionBorder( selectionColor: Color, + selectionBorderWidth: Dp, selectionAlpha: () -> Float = { 0f }, ): Modifier { return drawWithContent { drawContent() + + // Draw the border on the inside of the tile + val borderWidth = selectionBorderWidth.toPx() drawRoundRect( SolidColor(selectionColor), cornerRadius = CornerRadius(InactiveCornerRadius.toPx()), - style = Stroke(SelectedBorderWidth.toPx()), + topLeft = Offset(borderWidth / 2, borderWidth / 2), + size = Size(size.width - borderWidth, size.height - borderWidth), + style = Stroke(borderWidth), alpha = selectionAlpha(), ) } } @Composable +fun StaticTileBadge(icon: ImageVector, contentDescription: String?, onClick: () -> Unit) { + val offset = with(LocalDensity.current) { Offset(BadgeXOffset.toPx(), BadgeYOffset.toPx()) } + MinimumInteractiveSizeComponent(angle = { BADGE_ANGLE_RAD }, offset = { offset }) { + Box( + Modifier.fillMaxSize() + .clickable( + interactionSource = null, + indication = null, + onClickLabel = contentDescription, + onClick = onClick, + ) + ) { + val secondaryColor = MaterialTheme.colorScheme.secondary + Icon( + icon, + contentDescription = contentDescription, + modifier = + Modifier.size(BadgeSize).align(Alignment.Center).drawBehind { + drawCircle(secondaryColor) + }, + ) + } + } +} + +@Composable +private fun MinimumInteractiveSizeComponent( + angle: () -> Float, + offset: () -> Offset, + content: @Composable BoxScope.() -> Unit, +) { + // Use a higher zIndex than the tile to draw over it, and manually create the touch target + // as we're drawing over neighbor tiles as well. + val minTouchTargetSize = LocalMinimumInteractiveComponentSize.current + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.zIndex(2f) + .systemGestureExclusion { Rect(Offset.Zero, it.size.toSize()) } + .layout { measurable, constraints -> + val size = minTouchTargetSize.roundToPx() + val placeable = measurable.measure(Constraints.fixed(size, size)) + layout(placeable.width, placeable.height) { + val radius = constraints.maxHeight / 2f + val rotationCenter = Offset(constraints.maxWidth - radius, radius) + val position = offsetForAngle(angle(), radius, rotationCenter) + offset() + placeable.place( + position.x.roundToInt() - placeable.width / 2, + position.y.roundToInt() - placeable.height / 2, + ) + } + }, + content = content, + ) +} + +@Composable private fun Modifier.resizable(selected: Boolean, state: ResizingState): Modifier { if (!selected) return zIndex(1f) @@ -165,7 +256,69 @@ private fun Modifier.resizable(selected: Boolean, state: ResizingState): Modifie } } +enum class TileState { + None, + Removable, + Selected, +} + +@Composable +private fun Transition<TileState>.animateColor(): State<Color> { + return animateColor { state -> + when (state) { + None -> Color.Transparent + Removable -> MaterialTheme.colorScheme.secondary + Selected -> MaterialTheme.colorScheme.primary + } + } +} + +@Composable +private fun Transition<TileState>.animateAngle(): State<Float> { + return animateFloat { state -> + if (state == Removable) BADGE_ANGLE_RAD else RESIZING_PILL_ANGLE_RAD + } +} + +@Composable +private fun Transition<TileState>.animateSize(): State<Size> { + return animateSize { state -> + with(LocalDensity.current) { + when (state) { + None -> Size.Zero + Removable -> Size(BadgeSize.toPx()) + Selected -> Size(ResizingPillWidth.toPx(), ResizingPillHeight.toPx()) + } + } + } +} + +@Composable +private fun Transition<TileState>.animateOffset(): State<Offset> { + return animateOffset { state -> + with(LocalDensity.current) { + when (state) { + None -> Offset.Zero + Removable -> Offset(BadgeXOffset.toPx(), BadgeYOffset.toPx()) + Selected -> Offset(-SelectedBorderWidth.toPx(), 0f) + } + } + } +} + +private fun Size(size: Float) = Size(size, size) + +private fun offsetForAngle(angle: Float, radius: Float, center: Offset): Offset { + return Offset(x = radius * cos(angle) + center.x, y = radius * sin(angle) + center.y) +} + private object SelectionDefaults { - val ResizingDotSize = 16.dp val SelectedBorderWidth = 2.dp + val BadgeSize = 24.dp + val BadgeXOffset = -4.dp + val BadgeYOffset = 4.dp + val ResizingPillWidth = 8.dp + val ResizingPillHeight = 16.dp + const val BADGE_ANGLE_RAD = -.8f + const val RESIZING_PILL_ANGLE_RAD = 0f } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/EditModeButton.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/EditModeButton.kt index f3c06a481fc2..dc02fbb882af 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/EditModeButton.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/EditModeButton.kt @@ -29,17 +29,12 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.panels.ui.viewmodel.toolbar.EditModeButtonViewModel import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R @Composable -fun EditModeButton( - viewModelFactory: EditModeButtonViewModel.Factory, - modifier: Modifier = Modifier, -) { - val viewModel = rememberViewModel(traceName = "EditModeButton") { viewModelFactory.create() } +fun EditModeButton(viewModel: EditModeButtonViewModel, modifier: Modifier = Modifier) { CompositionLocalProvider( value = LocalContentColor provides MaterialTheme.colorScheme.onSurface ) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt index 7a2c51a49509..360266a31be3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt @@ -22,14 +22,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.android.systemui.compose.modifiers.sysuiResTag -import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.footer.ui.compose.IconButton import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel @Composable -fun Toolbar(toolbarViewModelFactory: ToolbarViewModel.Factory, modifier: Modifier = Modifier) { - val viewModel = rememberViewModel("Toolbar") { toolbarViewModelFactory.create() } - +fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { viewModel.userSwitcherViewModel?.let { IconButton( @@ -39,7 +36,7 @@ fun Toolbar(toolbarViewModelFactory: ToolbarViewModel.Factory, modifier: Modifie ) } - EditModeButton(viewModel.editModeButtonViewModelFactory) + EditModeButton(viewModel.editModeButtonViewModel) IconButton( viewModel.settingsButtonViewModel, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt index fe5eec848dd8..846e06846437 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.panels.ui.viewmodel -import androidx.compose.runtime.getValue import com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt index 1a6653c38f9f..e54bfa29d2db 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt @@ -45,7 +45,7 @@ import kotlinx.coroutines.launch class ToolbarViewModel @AssistedInject constructor( - val editModeButtonViewModelFactory: EditModeButtonViewModel.Factory, + editModeButtonViewModelFactory: EditModeButtonViewModel.Factory, private val footerActionsInteractor: FooterActionsInteractor, private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>, private val falsingInteractor: FalsingInteractor, @@ -83,6 +83,8 @@ constructor( ), ) + val editModeButtonViewModel: EditModeButtonViewModel = editModeButtonViewModelFactory.create() + override suspend fun onActivated(): Nothing { coroutineScope { launch { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt index d0f258052f04..76ff88506fb6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.tiles.impl.saver.domain -import android.content.Context import android.content.DialogInterface import android.content.SharedPreferences import android.os.Bundle @@ -24,7 +23,7 @@ import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.R import com.android.systemui.coroutines.newTracingContext import com.android.systemui.qs.tiles.impl.saver.domain.interactor.DataSaverTileUserActionInteractor -import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.DataSaverController import kotlin.coroutines.CoroutineContext @@ -32,13 +31,13 @@ import kotlinx.coroutines.CoroutineScope class DataSaverDialogDelegate( private val sysuiDialogFactory: SystemUIDialog.Factory, - @ShadeDisplayAware private val context: Context, + private val contextInteractor: ShadeDialogContextInteractor, private val backgroundContext: CoroutineContext, private val dataSaverController: DataSaverController, private val sharedPreferences: SharedPreferences, ) : SystemUIDialog.Delegate { override fun createDialog(): SystemUIDialog { - return sysuiDialogFactory.create(this, context) + return sysuiDialogFactory.create(this, contextInteractor.context) } override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt index 05bdf0a92679..63a9f594b05c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt @@ -22,7 +22,6 @@ import android.provider.Settings import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator -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.qs.tiles.base.actions.QSTileIntentUserInputHandler @@ -33,6 +32,7 @@ import com.android.systemui.qs.tiles.impl.saver.domain.model.DataSaverTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.settings.UserFileManager import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.DataSaverController import javax.inject.Inject @@ -44,6 +44,7 @@ class DataSaverTileUserActionInteractor @Inject constructor( @ShadeDisplayAware private val context: Context, + private val contextInteractor: ShadeDialogContextInteractor, @Main private val coroutineContext: CoroutineContext, @Background private val backgroundContext: CoroutineContext, private val dataSaverController: DataSaverController, @@ -79,18 +80,19 @@ constructor( val dialogDelegate = DataSaverDialogDelegate( systemUIDialogFactory, - context, + contextInteractor, backgroundContext, dataSaverController, - sharedPreferences + sharedPreferences, ) - val dialog = systemUIDialogFactory.create(dialogDelegate, context) + val dialog = + systemUIDialogFactory.create(dialogDelegate, contextInteractor.context) action.expandable ?.dialogTransitionController( DialogCuj( InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG + INTERACTION_JANK_TAG, ) ) ?.let { controller -> @@ -101,7 +103,7 @@ constructor( is QSTileUserAction.LongClick -> { qsTileIntentUserActionHandler.handle( action.expandable, - Intent(Settings.ACTION_DATA_SAVER_SETTINGS) + Intent(Settings.ACTION_DATA_SAVER_SETTINGS), ) } is QSTileUserAction.ToggleClick -> {} diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt index 68fd2f5a85d4..f9b1a36621b2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt @@ -20,6 +20,10 @@ import androidx.compose.runtime.getValue import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.ui.controller.MediaCarouselController +import com.android.systemui.media.controls.ui.view.MediaHost +import com.android.systemui.media.dagger.MediaModule import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel @@ -29,6 +33,7 @@ import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import javax.inject.Named import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.map @@ -41,10 +46,14 @@ constructor( shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, tileGridViewModelFactory: TileGridViewModel.Factory, @Assisted supportsBrightnessMirroring: Boolean, + @Assisted private val expansion: Float?, val editModeViewModel: EditModeViewModel, val detailsViewModel: DetailsViewModel, - val toolbarViewModelFactory: ToolbarViewModel.Factory, + toolbarViewModelFactory: ToolbarViewModel.Factory, shadeModeInteractor: ShadeModeInteractor, + mediaCarouselInteractor: MediaCarouselInteractor, + val mediaCarouselController: MediaCarouselController, + @Named(MediaModule.QS_PANEL) val mediaHost: MediaHost, ) : ExclusiveActivatable() { private val hydrator = Hydrator("QuickSettingsContainerViewModel.hydrator") @@ -52,6 +61,8 @@ constructor( val brightnessSliderViewModel = brightnessSliderViewModelFactory.create(supportsBrightnessMirroring) + val toolbarViewModel = toolbarViewModelFactory.create() + val shadeHeaderViewModel = shadeHeaderViewModelFactory.create() val tileGridViewModel = tileGridViewModelFactory.create() @@ -63,10 +74,18 @@ constructor( source = shadeModeInteractor.isShadeLayoutWide.map { !it }, ) + val showMedia: Boolean by + hydrator.hydratedStateOf( + traceName = "showMedia", + source = mediaCarouselInteractor.hasActiveMediaOrRecommendation, + ) + override suspend fun onActivated(): Nothing { coroutineScope { + expansion?.let { mediaHost.expansion = it } launch { hydrator.activate() } launch { brightnessSliderViewModel.activate() } + launch { toolbarViewModel.activate() } launch { shadeHeaderViewModel.activate() } launch { tileGridViewModel.activate() } awaitCancellation() @@ -75,6 +94,9 @@ constructor( @AssistedFactory interface Factory { - fun create(supportsBrightnessMirroring: Boolean): QuickSettingsContainerViewModel + fun create( + supportsBrightnessMirroring: Boolean, + expansion: Float? = null, + ): QuickSettingsContainerViewModel } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt index 862dba1e7294..e775231ef2a2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -65,8 +65,7 @@ constructor( * [userDetailViewAdapterProvider] and show it as launched from [expandable]. */ fun showDialog(expandable: Expandable) { - val context = shadeDialogContextInteractor.context - with(dialogFactory.create(context)) { + with(dialogFactory.create(shadeDialogContextInteractor.context)) { setShowForAllUsers(true) setCanceledOnTouchOutside(true) diff --git a/packages/SystemUI/src/com/android/systemui/rotationlock/DeviceStateAutoRotateModule.kt b/packages/SystemUI/src/com/android/systemui/rotationlock/DeviceStateAutoRotateModule.kt new file mode 100644 index 000000000000..628280210236 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/rotationlock/DeviceStateAutoRotateModule.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 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.rotationlock + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.policy.DeviceStateRotationLockSettingController +import com.android.window.flags.Flags +import dagger.Module +import dagger.Provides +import java.util.Optional +import javax.inject.Provider +import javax.inject.Qualifier + +@Module +class DeviceStateAutoRotateModule { + /** Qualifier for dependencies to be bound with [DeviceStateAutoRotateModule]. */ + @Qualifier + @MustBeDocumented + @Retention(AnnotationRetention.RUNTIME) + annotation class BoundsDeviceStateAutoRotateModule + + /** + * Provides an instance of [DeviceStateRotationLockSettingController]. + * + * @param controllerProvider The provider for [DeviceStateRotationLockSettingController]. + * @return An [Optional] containing the [DeviceStateRotationLockSettingController] instance if + * the `Flags.enableDeviceStateAutoRotateSettingRefactor()` flag is disabled, or an empty + * [Optional] otherwise. + */ + @Provides + @BoundsDeviceStateAutoRotateModule + @SysUISingleton + fun provideDeviceStateRotationLockSettingController( + controllerProvider: Provider<DeviceStateRotationLockSettingController> + ): Optional<DeviceStateRotationLockSettingController> = + if (Flags.enableDeviceStateAutoRotateSettingRefactor()) { + Optional.empty() + } else { + Optional.of(controllerProvider.get()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index d05837261b89..34b3324f81da 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -105,15 +105,14 @@ import com.android.systemui.util.kotlin.JavaAdapter; import dalvik.annotation.optimization.NeverCompile; -import dagger.Lazy; - -import kotlin.Unit; - import java.io.PrintWriter; import javax.inject.Inject; import javax.inject.Provider; +import dagger.Lazy; +import kotlin.Unit; + /** Handles QuickSettings touch handling, expansion and animation state. */ @SysUISingleton public class QuickSettingsControllerImpl implements QuickSettingsController, Dumpable { @@ -2366,8 +2365,16 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum return; } if (startTracing) { + if (mQs != null) { + mQs.setQSExpandingOrCollapsing(true); + } + monitor.begin(mPanelView, Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE); } else { + if (mQs != null) { + mQs.setQSExpandingOrCollapsing(false); + } + if (wasCancelled) { monitor.cancel(Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE); } else { diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt index 186bfcbbc8e2..a4de1d675a15 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt @@ -17,11 +17,19 @@ package com.android.systemui.shade.domain.interactor import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle import android.provider.AlarmClock +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.ActivityStarter import com.android.systemui.shade.data.repository.ShadeHeaderClockRepository +import com.android.systemui.util.kotlin.emitOnStart +import com.android.systemui.util.time.SystemClock +import java.util.Date import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map @SysUISingleton class ShadeHeaderClockInteractor @@ -29,7 +37,20 @@ class ShadeHeaderClockInteractor constructor( private val repository: ShadeHeaderClockRepository, private val activityStarter: ActivityStarter, + private val broadcastDispatcher: BroadcastDispatcher, + private val systemClock: SystemClock, ) { + /** [Flow] that emits `Unit` whenever the timezone or locale has changed. */ + val onTimezoneOrLocaleChanged: Flow<Unit> = + broadcastFlowForActions(Intent.ACTION_TIMEZONE_CHANGED, Intent.ACTION_LOCALE_CHANGED) + .emitOnStart() + + /** [Flow] that emits the current `Date` every minute, or when the system time has changed. */ + val currentTime: Flow<Date> = + broadcastFlowForActions(Intent.ACTION_TIME_TICK, Intent.ACTION_TIME_CHANGED) + .emitOnStart() + .map { Date(systemClock.currentTimeMillis()) } + /** Launch the clock activity. */ fun launchClockActivity() { val nextAlarmIntent = repository.nextAlarmIntent @@ -38,8 +59,22 @@ constructor( } else { activityStarter.postStartActivityDismissingKeyguard( Intent(AlarmClock.ACTION_SHOW_ALARMS), - 0 + 0, ) } } + + /** + * Returns a `Flow` that, when collected, emits `Unit` whenever a broadcast matching one of the + * given [actionsToFilter] is received. + */ + private fun broadcastFlowForActions( + vararg actionsToFilter: String, + user: UserHandle = UserHandle.SYSTEM, + ): Flow<Unit> { + return broadcastDispatcher.broadcastFlow( + filter = IntentFilter().apply { actionsToFilter.forEach(::addAction) }, + user = user, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index 8c38d2e7550c..20b44d73e097 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -18,16 +18,15 @@ package com.android.systemui.shade.ui.viewmodel import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.icu.text.DateFormat import android.icu.text.DisplayContext -import android.os.UserHandle import android.provider.Settings import android.view.ViewGroup +import androidx.compose.material3.ColorScheme import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.battery.BatteryMeterViewController -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.plugins.ActivityStarter @@ -42,7 +41,6 @@ import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeModeInteractor -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager @@ -50,18 +48,18 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIc import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import java.util.Date import java.util.Locale +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.mapLatest /** Models UI state for the shade header. */ +@OptIn(ExperimentalCoroutinesApi::class) class ShadeHeaderViewModel @AssistedInject constructor( @@ -70,16 +68,15 @@ constructor( private val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, private val shadeModeInteractor: ShadeModeInteractor, - private val mobileIconsInteractor: MobileIconsInteractor, + mobileIconsInteractor: MobileIconsInteractor, val mobileIconsViewModel: MobileIconsViewModel, private val privacyChipInteractor: PrivacyChipInteractor, private val clockInteractor: ShadeHeaderClockInteractor, private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, val statusBarIconController: StatusBarIconController, - val notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder, - private val broadcastDispatcher: BroadcastDispatcher, ) : ExclusiveActivatable() { + private val hydrator = Hydrator("ShadeHeaderViewModel.hydrator") val createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager = @@ -127,9 +124,16 @@ constructor( /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier - private val _mobileSubIds = MutableStateFlow(emptyList<Int>()) /** The list of subscription Ids for current mobile connections. */ - val mobileSubIds: StateFlow<List<Int>> = _mobileSubIds.asStateFlow() + val mobileSubIds: List<Int> by + hydrator.hydratedStateOf( + traceName = "mobileSubIds", + initialValue = emptyList(), + source = + mobileIconsInteractor.filteredSubscriptions.map { list -> + list.map { it.subscriptionId } + }, + ) /** The list of PrivacyItems to be displayed by the privacy chip. */ val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems @@ -150,45 +154,34 @@ constructor( private val longerPattern = context.getString(R.string.abbrev_wday_month_day_no_year_alarm) private val shorterPattern = context.getString(R.string.abbrev_month_day_no_year) - private val longerDateFormat = MutableStateFlow(getFormatFromPattern(longerPattern)) - private val shorterDateFormat = MutableStateFlow(getFormatFromPattern(shorterPattern)) - private val _shorterDateText: MutableStateFlow<String> = MutableStateFlow("") - val shorterDateText: StateFlow<String> = _shorterDateText.asStateFlow() + private val longerDateFormat: Flow<DateFormat> = + clockInteractor.onTimezoneOrLocaleChanged.mapLatest { getFormatFromPattern(longerPattern) } + private val shorterDateFormat: Flow<DateFormat> = + clockInteractor.onTimezoneOrLocaleChanged.mapLatest { getFormatFromPattern(shorterPattern) } - private val _longerDateText: MutableStateFlow<String> = MutableStateFlow("") - val longerDateText: StateFlow<String> = _longerDateText.asStateFlow() + val longerDateText: String by + hydrator.hydratedStateOf( + traceName = "longerDateText", + initialValue = "", + source = + combine(longerDateFormat, clockInteractor.currentTime) { format, time -> + format.format(time) + }, + ) + + val shorterDateText: String by + hydrator.hydratedStateOf( + traceName = "shorterDateText", + initialValue = "", + source = + combine(shorterDateFormat, clockInteractor.currentTime) { format, time -> + format.format(time) + }, + ) override suspend fun onActivated(): Nothing { coroutineScope { - launch { - broadcastDispatcher - .broadcastFlow( - filter = - IntentFilter().apply { - addAction(Intent.ACTION_TIME_TICK) - addAction(Intent.ACTION_TIME_CHANGED) - addAction(Intent.ACTION_TIMEZONE_CHANGED) - addAction(Intent.ACTION_LOCALE_CHANGED) - }, - user = UserHandle.SYSTEM, - map = { intent, _ -> - intent.action == Intent.ACTION_TIMEZONE_CHANGED || - intent.action == Intent.ACTION_LOCALE_CHANGED - }, - ) - .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) } - .launchIn(this) - } - - launch { updateDateTexts(false) } - - launch { - mobileIconsInteractor.filteredSubscriptions - .map { list -> list.map { it.subscriptionId } } - .collect { _mobileSubIds.value = it } - } - launch { hydrator.activate() } awaitCancellation() @@ -253,33 +246,34 @@ constructor( /** Represents the background highlight of a header icons chip. */ sealed interface HeaderChipHighlight { - data object None : HeaderChipHighlight - data object Weak : HeaderChipHighlight + fun backgroundColor(colorScheme: ColorScheme): Color - data object Strong : HeaderChipHighlight - } + fun foregroundColor(colorScheme: ColorScheme): Color + + data object None : HeaderChipHighlight { + override fun backgroundColor(colorScheme: ColorScheme): Color = Color.Unspecified + + override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.primary + } + + data object Weak : HeaderChipHighlight { + override fun backgroundColor(colorScheme: ColorScheme): Color = + colorScheme.primary.copy(alpha = 0.1f) - private fun updateDateTexts(invalidateFormats: Boolean) { - if (invalidateFormats) { - longerDateFormat.value = getFormatFromPattern(longerPattern) - shorterDateFormat.value = getFormatFromPattern(shorterPattern) + override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.primary } - val currentTime = Date() + data object Strong : HeaderChipHighlight { + override fun backgroundColor(colorScheme: ColorScheme): Color = colorScheme.secondary - _longerDateText.value = longerDateFormat.value.format(currentTime) - _shorterDateText.value = shorterDateFormat.value.format(currentTime) + override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.onSecondary + } } private fun getFormatFromPattern(pattern: String?): DateFormat { - val l = Locale.getDefault() - val format = DateFormat.getInstanceForSkeleton(pattern, l) - // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of - // CAPITALIZATION_FOR_STANDALONE is to address - // https://unicode-org.atlassian.net/browse/ICU-21631 - // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE - format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE) + val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault()) + format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE) return format } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 97de61969ffb..7dc2ae71b63e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -58,7 +58,6 @@ import android.view.KeyEvent; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.Appearance; import android.view.WindowInsetsController.Behavior; -import android.view.accessibility.Flags; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -987,13 +986,7 @@ public class CommandQueue extends IStatusBar.Stub implements @Override public void addQsTile(ComponentName tile) { - if (Flags.a11yQsShortcut()) { - addQsTileToFrontOrEnd(tile, false); - } else { - synchronized (mLock) { - mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget(); - } - } + addQsTileToFrontOrEnd(tile, false); } /** @@ -1003,13 +996,11 @@ public class CommandQueue extends IStatusBar.Stub implements */ @Override public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) { - if (Flags.a11yQsShortcut()) { - synchronized (mLock) { - SomeArgs args = SomeArgs.obtain(); - args.arg1 = tile; - args.arg2 = end; - mHandler.obtainMessage(MSG_ADD_QS_TILE, args).sendToTarget(); - } + synchronized (mLock) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = tile; + args.arg2 = end; + mHandler.obtainMessage(MSG_ADD_QS_TILE, args).sendToTarget(); } } @@ -1692,18 +1683,12 @@ public class CommandQueue extends IStatusBar.Stub implements } break; case MSG_ADD_QS_TILE: { - if (Flags.a11yQsShortcut()) { - SomeArgs someArgs = (SomeArgs) msg.obj; - for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).addQsTileToFrontOrEnd( - (ComponentName) someArgs.arg1, (boolean) someArgs.arg2); - } - someArgs.recycle(); - } else { - for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).addQsTile((ComponentName) msg.obj); - } + SomeArgs someArgs = (SomeArgs) msg.obj; + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).addQsTileToFrontOrEnd( + (ComponentName) someArgs.arg1, (boolean) someArgs.arg2); } + someArgs.recycle(); break; } case MSG_REMOVE_QS_TILE: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index 79a872edd2c5..bfd512fa6a2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -607,13 +607,6 @@ public final class KeyboardShortcutListSearch { context.getString(R.string.group_system_lock_screen), Arrays.asList( Pair.create(KeyEvent.KEYCODE_L, KeyEvent.META_META_ON))), - /* Pull up Notes app for quick memo: Meta + Ctrl + N */ - new ShortcutKeyGroupMultiMappingInfo( - context.getString(R.string.group_system_quick_memo), - Arrays.asList( - Pair.create( - KeyEvent.KEYCODE_N, - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON))), /* Access system settings: Meta + I */ new ShortcutKeyGroupMultiMappingInfo( context.getString(R.string.group_system_access_system_settings), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java index c1b8d9d123b9..6ebe02469f5c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar; +import static android.app.Flags.notificationsRedesignTemplates; + import android.app.Flags; import android.app.Notification; import android.graphics.drawable.Drawable; @@ -427,7 +429,8 @@ public class NotificationGroupingUtil { @Override public void apply(View parent, View view, boolean apply, boolean reset) { - if (reset && parent instanceof ConversationLayout) { + if (!notificationsRedesignTemplates() + && reset && parent instanceof ConversationLayout) { ConversationLayout layout = (ConversationLayout) parent; apply = layout.shouldHideAppName(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index f06565f1b6d2..32da6fff6bcc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -24,7 +24,7 @@ import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_ import static android.os.Flags.allowPrivateProfile; import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_NULL; -import static android.provider.Settings.Secure.REDACT_OTP_NOTIFICATION_IMMEDIATELY; +import static android.provider.Settings.Secure.OTP_NOTIFICATION_REDACTION_LOCK_TIME; import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS; import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS; import static android.provider.Settings.Secure.REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI; @@ -124,10 +124,10 @@ public class NotificationLockscreenUserManagerImpl implements private static final Uri REDACT_OTP_ON_WIFI = Settings.Secure.getUriFor(REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI); - private static final Uri REDACT_OTP_IMMEDIATELY = - Settings.Secure.getUriFor(REDACT_OTP_NOTIFICATION_IMMEDIATELY); + private static final Uri OTP_REDACTION_LOCK_TIME = + Settings.Secure.getUriFor(OTP_NOTIFICATION_REDACTION_LOCK_TIME); - private static final long LOCK_TIME_FOR_SENSITIVE_REDACTION_MS = + private static final long DEFAULT_LOCK_TIME_FOR_SENSITIVE_REDACTION_MS = TimeUnit.MINUTES.toMillis(10); private final Lazy<NotificationVisibilityProvider> mVisibilityProviderLazy; private final Lazy<CommonNotifCollection> mCommonNotifCollectionLazy; @@ -316,7 +316,8 @@ public class NotificationLockscreenUserManagerImpl implements protected final AtomicBoolean mConnectedToWifi = new AtomicBoolean(false); protected final AtomicBoolean mRedactOtpOnWifi = new AtomicBoolean(true); - protected final AtomicBoolean mRedactOtpImmediately = new AtomicBoolean(false); + protected final AtomicLong mOtpRedactionRequiredLockTimeMs = + new AtomicLong(DEFAULT_LOCK_TIME_FOR_SENSITIVE_REDACTION_MS); protected int mCurrentUserId = 0; @@ -375,7 +376,7 @@ public class NotificationLockscreenUserManagerImpl implements mLockScreenUris.add(SHOW_LOCKSCREEN); mLockScreenUris.add(SHOW_PRIVATE_LOCKSCREEN); mLockScreenUris.add(REDACT_OTP_ON_WIFI); - mLockScreenUris.add(REDACT_OTP_IMMEDIATELY); + mLockScreenUris.add(OTP_REDACTION_LOCK_TIME); dumpManager.registerDumpable(this); @@ -447,8 +448,8 @@ public class NotificationLockscreenUserManagerImpl implements changed |= updateUserShowPrivateSettings(user.getIdentifier()); } else if (REDACT_OTP_ON_WIFI.equals(uri)) { changed |= updateRedactOtpOnWifiSetting(); - } else if (REDACT_OTP_IMMEDIATELY.equals(uri)) { - changed |= updateRedactOtpImmediatelySetting(); + } else if (OTP_REDACTION_LOCK_TIME.equals(uri)) { + changed |= updateOtpLockTimeSetting(); } } @@ -487,7 +488,7 @@ public class NotificationLockscreenUserManagerImpl implements mLockscreenSettingsObserver ); mSecureSettings.registerContentObserverAsync( - REDACT_OTP_IMMEDIATELY, + OTP_REDACTION_LOCK_TIME, mLockscreenSettingsObserver ); @@ -638,13 +639,13 @@ public class NotificationLockscreenUserManagerImpl implements } @WorkerThread - private boolean updateRedactOtpImmediatelySetting() { - boolean originalValue = mRedactOtpImmediately.get(); - boolean newValue = mSecureSettings.getIntForUser( - REDACT_OTP_NOTIFICATION_IMMEDIATELY, - 0, - Process.myUserHandle().getIdentifier()) != 0; - mRedactOtpImmediately.set(newValue); + private boolean updateOtpLockTimeSetting() { + long originalValue = mOtpRedactionRequiredLockTimeMs.get(); + long newValue = mSecureSettings.getLongForUser( + OTP_NOTIFICATION_REDACTION_LOCK_TIME, + DEFAULT_LOCK_TIME_FOR_SENSITIVE_REDACTION_MS, + Process.myUserHandle().getIdentifier()); + mOtpRedactionRequiredLockTimeMs.set(newValue); return originalValue != newValue; } @@ -832,14 +833,9 @@ public class NotificationLockscreenUserManagerImpl implements return false; } - long latestTimeForRedaction; - if (mRedactOtpImmediately.get()) { - latestTimeForRedaction = mLastLockTime.get(); - } else { - // If the lock screen was not already locked for LOCK_TIME_FOR_SENSITIVE_REDACTION_MS - // when this notification arrived, do not redact - latestTimeForRedaction = mLastLockTime.get() + LOCK_TIME_FOR_SENSITIVE_REDACTION_MS; - } + // If the lock screen was not already locked for at least mOtpRedactionRequiredLockTimeMs + // when this notification arrived, do not redact + long latestTimeForRedaction = mLastLockTime.get() + mOtpRedactionRequiredLockTimeMs.get(); if (ent.getSbn().getPostTime() < latestTimeForRedaction) { return false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 6aa2fe29e768..3db004848d22 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -110,6 +110,10 @@ constructor( private var prevTimestamp: Long = -1 private var prevShadeDirection = 0 private var prevShadeVelocity = 0f + // tracks whether app launch transition is in progress. This involves two independent factors + // that control blur, shade expansion and app launch animation from outside sysui. + // They can complete out of order, this flag will be reset by the animation that finishes later. + private var appLaunchTransitionIsInProgress = false // Only for dumpsys private var lastAppliedBlur = 0 @@ -158,6 +162,18 @@ constructor( if (field == value) { return } + // Set this to true now, this will be reset when the next shade expansion finishes or + // when the app launch finishes, whichever happens later. + if (value) { + appLaunchTransitionIsInProgress = true + } else { + // App was launching and now it has finished launching + if (shadeExpansion == 0.0f) { + // this means shade expansion finished before app launch was done. + // reset the flag here + appLaunchTransitionIsInProgress = false + } + } field = value scheduleUpdate() @@ -172,6 +188,12 @@ constructor( shadeAnimation.animateTo(0) shadeAnimation.finishIfRunning() } + @Deprecated( + message = + "This might get reset to false before shade expansion is fully done, " + + "consider using areBlursDisabledForAppLaunch" + ) + get() = field private var zoomOutCalculatedFromShadeRadius: Float = 0.0f @@ -183,6 +205,11 @@ constructor( scheduleUpdate() } + private val areBlursDisabledForAppLaunch: Boolean + get() = + blursDisabledForAppLaunch || + (Flags.bouncerUiRevamp() && appLaunchTransitionIsInProgress) + /** Force stop blur effect when necessary. */ private var scrimsVisible: Boolean = false set(value) { @@ -221,7 +248,7 @@ constructor( combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(transitionToFullShadeProgress)) var shadeRadius = max(combinedBlur, wakeAndUnlockBlurRadius) - if (blursDisabledForAppLaunch || blursDisabledForUnlock) { + if (areBlursDisabledForAppLaunch || blursDisabledForUnlock) { shadeRadius = 0f } @@ -259,7 +286,7 @@ constructor( private val shouldBlurBeOpaque: Boolean get() = if (Flags.notificationShadeBlur()) false - else scrimsVisible && !blursDisabledForAppLaunch + else scrimsVisible && !areBlursDisabledForAppLaunch /** Callback that updates the window blur value and is called only once per frame. */ @VisibleForTesting @@ -442,6 +469,13 @@ constructor( val shadeDirection = sign(diff).toInt() val shadeVelocity = MathUtils.constrain(VELOCITY_SCALE * diff / deltaTime, MIN_VELOCITY, MAX_VELOCITY) + if (expansion == 0.0f && appLaunchTransitionIsInProgress && !blursDisabledForAppLaunch) { + // Shade expansion finished but the app launch is already done, then this should mark + // the transition as done. + Log.d(TAG, "appLaunchTransitionIsInProgress is now false from shade expansion event") + appLaunchTransitionIsInProgress = false + } + updateShadeAnimationBlur(expansion, tracking, shadeVelocity, shadeDirection) prevShadeDirection = shadeDirection @@ -553,6 +587,7 @@ constructor( it.println("brightnessMirrorRadius: ${brightnessMirrorSpring.radius}") it.println("wakeAndUnlockBlur: $wakeAndUnlockBlurRadius") it.println("blursDisabledForAppLaunch: $blursDisabledForAppLaunch") + it.println("appLaunchTransitionIsInProgress: $appLaunchTransitionIsInProgress") it.println("qsPanelExpansion: $qsPanelExpansion") it.println("transitionToFullShadeProgress: $transitionToFullShadeProgress") it.println("lastAppliedBlur: $lastAppliedBlur") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt index 2eae3eb4fc19..7548f6ff5bd2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt @@ -22,9 +22,9 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository import com.android.systemui.statusbar.phone.ongoingcall.domain.interactor.OngoingCallInteractor -import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -44,25 +44,17 @@ constructor( @StatusBarChipsLog private val logger: LogBuffer, ) { val ongoingCallState: StateFlow<OngoingCallModel> = - (if (StatusBarChipsModernization.isEnabled) - ongoingCallInteractor.ongoingCallState - else - repository.ongoingCallState) + (if (StatusBarChipsModernization.isEnabled) { + ongoingCallInteractor.ongoingCallState + } else { + repository.ongoingCallState + }) .onEach { - logger.log( - TAG, - LogLevel.INFO, - { str1 = it::class.simpleName }, - { "State: $str1" } - ) + logger.log(TAG, LogLevel.INFO, { str1 = it::class.simpleName }, { "State: $str1" }) } - .stateIn( - scope, - SharingStarted.Lazily, - OngoingCallModel.NoCall - ) + .stateIn(scope, SharingStarted.Lazily, OngoingCallModel.NoCall) companion object { private val TAG = "OngoingCall".pad() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java index dbcda418496e..cfc0055954b9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java @@ -32,12 +32,16 @@ public class WifiIcons { */ private static int[] getIconsBasedOnFlag() { if (newStatusBarIcons()) { + // TODO(b/396664075): + // The new wifi icons only define a range of [0, 3]. Since this array is indexed on + // level, we can simulate the range squash by mapping both level 3 to drawn-level 2, and + // level 4 to drawn-level 3 return new int[] { R.drawable.ic_wifi_0, R.drawable.ic_wifi_1, R.drawable.ic_wifi_2, - R.drawable.ic_wifi_3, - R.drawable.ic_wifi_4 + R.drawable.ic_wifi_2, + R.drawable.ic_wifi_3 }; } else { return new int[] { @@ -54,12 +58,13 @@ public class WifiIcons { private static int [] getErrorIconsBasedOnFlag() { if (newStatusBarIcons()) { + // See above note, new wifi icons only have 3 bars, so levels 2 and 3 are the same return new int[] { R.drawable.ic_wifi_0_error, R.drawable.ic_wifi_1_error, R.drawable.ic_wifi_2_error, + R.drawable.ic_wifi_2_error, R.drawable.ic_wifi_3_error, - R.drawable.ic_wifi_4_error }; } else { return new int[] { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt index e3be95373698..402881d438dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt @@ -29,10 +29,10 @@ object NewStatusBarIcons { val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled) - /** Is the refactor enabled */ + /** Is the refactor enabled. Dependency on [StatusBarRootModernization] */ @JvmStatic inline val isEnabled - get() = Flags.newStatusBarIcons() + get() = Flags.newStatusBarIcons() && StatusBarRootModernization.isEnabled /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 7b5f5f6d8060..e6dd09b1e1b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -97,7 +97,7 @@ constructor( private val secureSettings: SecureSettings, private val userTracker: UserTracker, private val contentResolver: ContentResolver, - private val configurationController: ConfigurationController, + @ShadeDisplayAware private val configurationController: ConfigurationController, private val statusBarStateController: StatusBarStateController, private val deviceProvisionedController: DeviceProvisionedController, private val bypassController: KeyguardBypassController, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java index 0e3f103c152e..24ab6959b9e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java @@ -21,9 +21,14 @@ import static android.app.NotificationChannel.PROMOTIONS_ID; import static android.app.NotificationChannel.RECS_ID; import static android.app.NotificationChannel.SOCIAL_MEDIA_ID; +import android.app.Notification; +import android.content.Context; +import android.os.Build; + import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.systemui.statusbar.notification.icon.IconPack; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import java.util.List; @@ -79,6 +84,43 @@ public class BundleEntry extends PipelineEntry { public EntryAdapter getGroupRoot() { return this; } + + @Override + public boolean isClearable() { + // TODO(b/394483200): check whether all of the children are clearable, when implemented + return true; + } + + @Override + public int getTargetSdk() { + return Build.VERSION_CODES.CUR_DEVELOPMENT; + } + + @Override + public String getSummarization() { + return null; + } + + @Override + public int getContrastedColor(Context context, boolean isLowPriority, int backgroundColor) { + return Notification.COLOR_DEFAULT; + } + + @Override + public boolean canPeek() { + return false; + } + + @Override + public long getWhen() { + return 0; + } + + @Override + public IconPack getIcons() { + // TODO(b/396446620): implement bundle icons + return null; + } } public static final List<BundleEntry> ROOT_BUNDLES = List.of( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index 4df81c97e21e..6431cacf2107 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -16,9 +16,12 @@ package com.android.systemui.statusbar.notification.collection; +import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.systemui.statusbar.notification.icon.IconPack; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; /** @@ -59,9 +62,49 @@ public interface EntryAdapter { EntryAdapter getGroupRoot(); /** + * @return whether the row can be removed with the 'Clear All' action + */ + boolean isClearable(); + + /** * Returns whether the entry is attached to the current shade list */ default boolean isAttached() { return getParent() != null; } + + /** + * Returns the target sdk of the package that owns this entry. + */ + int getTargetSdk(); + + /** + * Returns the summarization for this entry, if there is one + */ + @Nullable String getSummarization(); + + /** + * Performs any steps needed to set or reset data before an inflation or reinflation. + */ + default void prepareForInflation() {} + + /** + * Gets a color that would have sufficient contrast on the given background color. + */ + int getContrastedColor(Context context, boolean isLowPriority, int backgroundColor); + + /** + * Whether this entry can peek on screen as a heads up view + */ + boolean canPeek(); + + /** + * Returns the visible 'time', in milliseconds, of the entry + */ + long getWhen(); + + /** + * Retrieves the pack of icons associated with this entry + */ + IconPack getIcons(); } 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 90f9525c7683..698a56363465 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 @@ -78,6 +78,7 @@ import com.android.systemui.statusbar.notification.row.NotificationGuts; import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel; import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel; import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.PriorityBucket; import com.android.systemui.util.ListenerSet; @@ -309,6 +310,47 @@ public final class NotificationEntry extends ListEntry { } return null; } + + @Override + public boolean isClearable() { + return NotificationEntry.this.isClearable(); + } + + @Override + public int getTargetSdk() { + return NotificationEntry.this.targetSdk; + } + + @Override + public String getSummarization() { + return getRanking().getSummarization(); + } + + @Override + public void prepareForInflation() { + getSbn().clearPackageContext(); + } + + @Override + public int getContrastedColor(Context context, boolean isLowPriority, int backgroundColor) { + return NotificationEntry.this.getContrastedColor( + context, isLowPriority, backgroundColor); + } + + @Override + public boolean canPeek() { + return isStickyAndNotDemoted(); + } + + @Override + public long getWhen() { + return getSbn().getNotification().getWhen(); + } + + @Override + public IconPack getIcons() { + return NotificationEntry.this.getIcons(); + } } public EntryAdapter getEntryAdapter() { @@ -580,6 +622,7 @@ public final class NotificationEntry extends ListEntry { } public boolean hasFinishedInitialization() { + NotificationBundleUi.assertInLegacyMode(); return initializationTime != -1 && SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY; } @@ -663,10 +706,12 @@ public final class NotificationEntry extends ListEntry { } public void resetInitializationTime() { + NotificationBundleUi.assertInLegacyMode(); initializationTime = -1; } public void setInitializationTime(long time) { + NotificationBundleUi.assertInLegacyMode(); if (initializationTime == -1) { initializationTime = time; } @@ -683,9 +728,13 @@ public final class NotificationEntry extends ListEntry { * @return {@code true} if we are a media notification */ public boolean isMediaNotification() { - if (row == null) return false; + if (NotificationBundleUi.isEnabled()) { + return getSbn().getNotification().isMediaNotification(); + } else { + if (row == null) return false; - return row.isMediaRow(); + return row.isMediaRow(); + } } public boolean containsCustomViews() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index 9c1d0735a65b..ac11c15e8bf8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -67,6 +67,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable; import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; import com.android.systemui.util.Assert; import com.android.systemui.util.NamedListenerSet; @@ -1282,7 +1283,13 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { entry.getAttachState().setExcludingFilter(filter); if (filter != null) { // notification is removed from the list, so we reset its initialization time - entry.resetInitializationTime(); + if (NotificationBundleUi.isEnabled()) { + if (entry.getRow() != null) { + entry.getRow().resetInitializationTime(); + } + } else { + entry.resetInitializationTime(); + } } return filter != null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt index dba8a9a6ddec..867db8ec8235 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt @@ -163,6 +163,7 @@ constructor( if (entry in nextMap) nextMap.remove(entry) if (entry in nextList) nextList.remove(entry) + outcome = "add next" addToNext(entry, runnable) // Shorten headsUpEntryShowing display time @@ -174,7 +175,7 @@ constructor( headsUpEntryShowing!!.updateEntry( /* updatePostTime= */ false, /* updateEarliestRemovalTime= */ false, - /* reason= */ "avalanche duration update", + /* reason= */ "shorten duration of previously-last HUN", ) } } @@ -269,8 +270,10 @@ constructor( } nextList.sort() val entryList = showingList + nextList + val thisKey = getKey(entry) if (entryList.isEmpty()) { - log { "No avalanche HUNs, use default ms: $autoDismissMs" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, autoDismissMs, "No avalanche HUNs, use default", nextKey = "") return autoDismissMs } // entryList.indexOf(entry) returns -1 even when the entry is in entryList @@ -281,27 +284,29 @@ constructor( } } if (thisEntryIndex == -1) { - log { "Untracked entry, use default ms: $autoDismissMs" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, autoDismissMs, "Untracked entry, use default", nextKey = "") return autoDismissMs } val nextEntryIndex = thisEntryIndex + 1 - - // If last entry, use default duration if (nextEntryIndex >= entryList.size) { - log { "Last entry, use default ms: $autoDismissMs" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, autoDismissMs, "Last entry, use default", nextKey = "") return autoDismissMs } val nextEntry = entryList[nextEntryIndex] + val nextKey = getKey(nextEntry) if (nextEntry.compareNonTimeFields(entry) == -1) { - // Next entry is higher priority - log { "Next entry is higher priority: 500ms" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, 500, "LOWER priority than next: ", nextKey) return 500 } else if (nextEntry.compareNonTimeFields(entry) == 0) { - // Next entry is same priority - log { "Next entry is same priority: 1000ms" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, 1000, "SAME priority as next: ", nextKey) return 1000 } else { - log { "Next entry is lower priority, use default ms: $autoDismissMs" } + headsUpManagerLogger.logAvalancheDuration( + thisKey, autoDismissMs, "HIGHER priority than next: ", nextKey) return autoDismissMs } } @@ -355,25 +360,28 @@ constructor( } private fun showNow(entry: HeadsUpEntry, runnableList: MutableList<Runnable>) { - log { "SHOW: " + getKey(entry) } - + headsUpManagerLogger.logAvalancheStage("show", getKey(entry)) uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN) headsUpEntryShowing = entry - runnableList.forEach { - if (it in debugRunnableLabelMap) { - log { "RUNNABLE: ${debugRunnableLabelMap[it]}" } + runnableList.forEach { runnable -> + if (debug) { + debugRunnableLabelMap[runnable]?.let { label -> + headsUpManagerLogger.logAvalancheStage("run", label) + // Remove label after logging to avoid memory leak + debugRunnableLabelMap.remove(runnable) + } } - it.run() + runnable.run() } } private fun showNext() { - log { "SHOW NEXT" } + headsUpManagerLogger.logAvalancheStage("show next", key = "") headsUpEntryShowing = null if (nextList.isEmpty()) { - log { "NO MORE TO SHOW" } + headsUpManagerLogger.logAvalancheStage("no more", key = "") previousHunKey = "" return } @@ -395,11 +403,7 @@ constructor( debugRunnableLabelMap.remove(r) } } - val queue = ArrayList<String>() - for (entry in listToDrop) { - queue.add("[${getKey(entry)}]") - } - val dropList = java.lang.String.join("\n", queue) + val dropList = listToDrop.joinToString("\n ") { getKey(it) } headsUpManagerLogger.logDroppedHuns(dropList) } clearNext() @@ -424,38 +428,24 @@ constructor( // Methods below are for logging only ========================================================== - private inline fun log(s: () -> String) { - if (debug) { - Log.d(tag, s()) - } - } - private fun getStateStr(): String { - return "\navalanche state:" + - "\n\tshowing: [${getKey(headsUpEntryShowing)}]" + - "\n\tprevious: [$previousHunKey]" + - "\n\tnext list: $nextListStr" + - "\n\tnext map: $nextMapStr" + - "\nBHUM.mHeadsUpEntryMap: " + - baseEntryMapStr() + return "\n[AC state]" + + "\nshow: ${getKey(headsUpEntryShowing)}" + + "\nprevious: $previousHunKey" + + "\n$nextStr" + + "\n[HeadsUpManagerImpl.mHeadsUpEntryMap] " + baseEntryMapStr() + "\n" } - private val nextListStr: String + private val nextStr: String get() { - val queue = ArrayList<String>() - for (entry in nextList) { - queue.add("[${getKey(entry)}]") + val nextListStr = nextList.joinToString("\n ") { getKey(it) } + if (nextList.toSet() == nextMap.keys.toSet()) { + return "next (${nextList.size}):\n $nextListStr" } - return java.lang.String.join("\n", queue) - } - - private val nextMapStr: String - get() { - val queue = ArrayList<String>() - for (entry in nextMap.keys) { - queue.add("[${getKey(entry)}]") - } - return java.lang.String.join("\n", queue) + // This should never happen + val nextMapStr = nextMap.keys.joinToString("\n ") { getKey(it) } + return "next list (${nextList.size}):\n $nextListStr" + + "\nnext map (${nextMap.size}):\n $nextMapStr" } fun getKey(entry: HeadsUpEntry?): String { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index 7d74a496853f..87b9bf87c680 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -856,11 +856,11 @@ public class HeadsUpManagerImpl private String getEntryMapStr() { if (mHeadsUpEntryMap.isEmpty()) { - return "EMPTY"; + return ""; } StringBuilder entryMapStr = new StringBuilder(); for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) { - entryMapStr.append("\n\t").append( + entryMapStr.append("\n ").append( entry.mEntry == null ? "null" : entry.mEntry.getKey()); } return entryMapStr.toString(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt index 65fb9ca558d7..388d357b3b15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt @@ -71,7 +71,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { str3 = outcome bool1 = isEnabled }, - { "$str1\n\t=> AC[isEnabled:$bool1] update: $str2\n\t=> $str3" }, + { "$str1\n=> AC[enabled:$bool1] update: $str2\n=> $str3" }, ) } @@ -90,7 +90,33 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { str3 = outcome bool1 = isEnabled }, - { "$str1\n\t=> AC[isEnabled:$bool1] delete: $str2\n\t=> $str3" }, + { "$str1\n=> AC[enabled:$bool1] delete: $str2\n=> $str3" }, + ) + } + + fun logAvalancheStage(stage: String, key: String) { + buffer.log( + TAG, + INFO, + { + str1 = stage + str2 = key + }, + { "[AC] $str1 $str2" }, + ) + } + + fun logAvalancheDuration(thisKey: String, duration: Int, reason: String, nextKey: String) { + buffer.log( + TAG, + INFO, + { + str1 = thisKey + int1 = duration + str2 = reason + str3 = nextKey + }, + { "[AC] $str1 | $int1 ms | $str2 $str3" }, ) } @@ -325,7 +351,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { } fun logDroppedHuns(entryList: String) { - buffer.log(TAG, VERBOSE, { str1 = entryList }, { "[AC] Drop HUNs: $str1" }) + buffer.log(TAG, VERBOSE, { str1 = entryList }, { "[AC] dropped:\n $str1" }) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt index 9aa5a2e32ada..7959e99f3189 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.promoted import android.app.Flags +import android.app.Flags.notificationsRedesignTemplates import android.app.Notification import android.graphics.PorterDuff import android.view.LayoutInflater @@ -166,7 +167,10 @@ private class AODPromotedNotificationViewUpdater(root: View) { private val closeButton: View? = root.findViewById(R.id.close_button) private val conversationIconBadge: View? = root.findViewById(R.id.conversation_icon_badge) private val conversationIcon: CachingIconView? = root.findViewById(R.id.conversation_icon) - private val conversationText: TextView? = root.findViewById(R.id.conversation_text) + private val conversationText: TextView? = + root.findViewById( + if (notificationsRedesignTemplates()) R.id.title else R.id.conversation_text + ) private val expandButton: NotificationExpandButton? = root.findViewById(R.id.expand_button) private val headerText: TextView? = root.findViewById(R.id.header_text) private val headerTextDivider: View? = root.findViewById(R.id.header_text_divider) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index b4092cd785bf..b0773276a3e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row; import static com.android.systemui.Flags.notificationBackgroundTintOptimization; +import static com.android.systemui.Flags.notificationRowTransparency; import static com.android.systemui.statusbar.notification.row.ExpandableView.ClipSide.BOTTOM; import static com.android.systemui.statusbar.notification.row.ExpandableView.ClipSide.TOP; @@ -38,6 +39,7 @@ import com.android.app.animation.Interpolators; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; import com.android.systemui.Gefingerpoken; +import com.android.systemui.common.shared.colors.SurfaceEffectColors; import com.android.systemui.res.R; import com.android.systemui.shade.TouchLogger; import com.android.systemui.statusbar.NotificationShelf; @@ -122,8 +124,12 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } private void updateColors() { - mNormalColor = mContext.getColor( - com.android.internal.R.color.materialColorSurfaceContainerHigh); + if (notificationRowTransparency()) { + mNormalColor = SurfaceEffectColors.surfaceEffect1(getResources()); + } else { + mNormalColor = mContext.getColor( + com.android.internal.R.color.materialColorSurfaceContainerHigh); + } mTintedRippleColor = mContext.getColor( R.color.notification_ripple_tinted_color); mNormalRippleColor = mContext.getColor( 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 66a0fb4ee4ab..6f7eade1c0f2 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 @@ -21,6 +21,7 @@ import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_ import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static com.android.systemui.Flags.notificationsPinnedHunInShade; +import static com.android.systemui.Flags.notificationRowTransparency; import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE; import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; @@ -45,6 +46,7 @@ import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; +import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; @@ -116,7 +118,6 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.logging.NotificationCounters; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; -import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; import com.android.systemui.statusbar.notification.row.wrapper.NotificationCompactMessagingTemplateViewWrapper; @@ -168,6 +169,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private static final long RECENTLY_ALERTED_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); private static final SourceType BASE_VALUE = SourceType.from("BaseValue"); private static final SourceType FROM_PARENT = SourceType.from("FromParent(ENR)"); + private static final long INITIALIZATION_DELAY = 400; // We don't correctly track dark mode until the content views are inflated, so always update // the background on first content update just in case it happens to be during a theme change. @@ -266,7 +268,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private NotificationContentView mPublicLayout; private NotificationContentView mPrivateLayout; private NotificationContentView[] mLayouts; - private int mNotificationColor; private ExpandableNotificationRowLogger mLogger; private String mLoggingKey; private NotificationGuts mGuts; @@ -351,6 +352,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView */ private boolean mSaveSpaceOnLockscreen; + // indicates when this view was first attached to a window + // this value will reset when the view is completely removed from the shade (ie: filtered out) + private long initializationTime = -1; + /** * It is added for unit testing purpose. * Please do not use it for other purposes. @@ -624,26 +629,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** - * Marks a content view as freeable, setting it so that future inflations do not reinflate - * and ensuring that the view is freed when it is safe to remove. - * - * @param inflationFlag flag corresponding to the content view to be freed - * @deprecated By default, {@link NotificationRowContentBinder#unbindContent} will tell the - * view hierarchy to only free when the view is safe to remove so this method is no longer - * needed. Will remove when all uses are gone. - */ - @Deprecated - public void freeContentViewWhenSafe(@InflationFlag int inflationFlag) { - RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); - params.markContentViewsFreeable(inflationFlag); - mRowContentBindStage.requestRebind(mEntry, null /* callback */); - } - - /** * Returns whether this row is considered non-blockable (i.e. it's a non-blockable system notif * or is in an allowList). */ public boolean getIsNonblockable() { + NotificationBundleUi.assertInLegacyMode(); if (mEntry == null) { return true; } @@ -665,9 +655,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView l.onNotificationUpdated(mEntry); } mShowingPublicInitialized = false; - updateNotificationColor(); if (mMenuRow != null) { - mMenuRow.onNotificationUpdated(mEntry.getSbn()); + mMenuRow.onNotificationUpdated(); mMenuRow.setAppName(mAppName); } if (mIsSummaryWithChildren) { @@ -726,15 +715,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** - * Called when the notification's ranking was changed (but nothing else changed). - */ - public void onNotificationRankingUpdated() { - if (mMenuRow != null) { - mMenuRow.onNotificationUpdated(mEntry.getSbn()); - } - } - - /** * Call when bubble state has changed and the button on the notification should be updated. */ public void updateBubbleButton() { @@ -745,7 +725,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @VisibleForTesting void updateShelfIconColor() { - StatusBarIconView expandedIcon = mEntry.getIcons().getShelfIcon(); + StatusBarIconView expandedIcon = getShelfIcon(); boolean isPreL = Boolean.TRUE.equals(expandedIcon.getTag(R.id.icon_is_pre_L)); boolean colorize = !isPreL || NotificationUtils.isGrayscale(expandedIcon, ContrastColorUtil.getInstance(mContext)); @@ -766,8 +746,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (color != Notification.COLOR_INVALID) { return color; } else { - return mEntry.getContrastedColor(mContext, mIsMinimized && !isExpanded(), - getBackgroundColorWithoutTint()); + if (NotificationBundleUi.isEnabled()) { + return mEntryAdapter.getContrastedColor(mContext, mIsMinimized && !isExpanded(), + getBackgroundColorWithoutTint()); + } else { + return mEntry.getContrastedColor(mContext, mIsMinimized && !isExpanded(), + getBackgroundColorWithoutTint()); + } } } @@ -869,15 +854,29 @@ public class ExpandableNotificationRow extends ActivatableNotificationView boolean customView = contractedView != null && contractedView.getId() != com.android.internal.R.id.status_bar_latest_event_content; - boolean beforeN = mEntry.targetSdk < Build.VERSION_CODES.N; - boolean beforeP = mEntry.targetSdk < Build.VERSION_CODES.P; - boolean beforeS = mEntry.targetSdk < Build.VERSION_CODES.S; + int targetSdk = Build.VERSION_CODES.CUR_DEVELOPMENT; + if (NotificationBundleUi.isEnabled()) { + targetSdk = mEntryAdapter.getTargetSdk(); + } else { + targetSdk = mEntry.targetSdk; + } + + boolean beforeN = targetSdk < Build.VERSION_CODES.N; + boolean beforeP = targetSdk < Build.VERSION_CODES.P; + boolean beforeS = targetSdk < Build.VERSION_CODES.S; int smallHeight; boolean isCallLayout = contractedView instanceof CallLayout; boolean isMessagingLayout = contractedView instanceof MessagingLayout || contractedView instanceof ConversationLayout; + String summarization = null; + if (NotificationBundleUi.isEnabled()) { + summarization = mEntryAdapter.getSummarization(); + } else { + summarization = mEntry.getRanking().getSummarization(); + } + if (customView && beforeS && !mIsSummaryWithChildren) { if (beforeN) { smallHeight = mMaxSmallHeightBeforeN; @@ -890,7 +889,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView smallHeight = maxExpandedHeight; } else if (NmSummarizationUiFlag.isEnabled() && isMessagingLayout - && !TextUtils.isEmpty(mEntry.getRanking().getSummarization())) { + && !TextUtils.isEmpty(summarization)) { smallHeight = mMaxSmallHeightWithSummarization; } else { smallHeight = mMaxSmallHeight; @@ -1523,7 +1522,22 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public boolean hasFinishedInitialization() { - return getEntry().hasFinishedInitialization(); + if (NotificationBundleUi.isEnabled()) { + return initializationTime != -1 + && SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY; + } else { + return getEntry().hasFinishedInitialization(); + } + } + + public void resetInitializationTime() { + initializationTime = -1; + } + + public void setInitializationTime(long time) { + if (initializationTime == -1) { + initializationTime = time; + } } /** @@ -1538,7 +1552,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return null; } if (mMenuRow.getMenuView() == null) { - mMenuRow.createMenu(this, mEntry.getSbn()); + mMenuRow.createMenu(this); mMenuRow.setAppName(mAppName); FrameLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); @@ -1580,7 +1594,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (oldMenu != null) { int menuIndex = indexOfChild(oldMenu); removeView(oldMenu); - mMenuRow.createMenu(ExpandableNotificationRow.this, mEntry.getSbn()); + mMenuRow.createMenu(ExpandableNotificationRow.this); mMenuRow.setAppName(mAppName); addView(mMenuRow.getMenuView(), menuIndex); } @@ -1588,12 +1602,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView l.reinflate(); l.reInflateViews(); } - mEntry.getSbn().clearPackageContext(); + if (NotificationBundleUi.isEnabled()) { + mEntryAdapter.prepareForInflation(); + } else { + mEntry.getSbn().clearPackageContext(); + } // TODO: Move content inflation logic out of this call RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); params.setNeedsReinflation(true); - var rebindEndCallback = mRebindingTracker.trackRebinding(mEntry.getKey()); + var rebindEndCallback = mRebindingTracker.trackRebinding(NotificationBundleUi.isEnabled() + ? mEntryAdapter.getKey() : mEntry.getKey()); mRowContentBindStage.requestRebind(mEntry, (e) -> rebindEndCallback.onFinished()); Trace.endSection(); } @@ -1636,6 +1655,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (view != null) { view.setBackgroundTintColor(color); } + if (notificationRowTransparency() + && (mBackgroundNormal != null) + && (mEntry != null)) { + mBackgroundNormal.setBgIsColorized( + mEntry.getSbn().getNotification().isColorized()); + } } public void closeRemoteInput() { @@ -1651,20 +1676,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPrivateLayout.setSingleLineWidthIndention(indention); } - public int getNotificationColor() { - return mNotificationColor; - } - - public void updateNotificationColor() { - Configuration currentConfig = getResources().getConfiguration(); - boolean nightMode = (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) - == Configuration.UI_MODE_NIGHT_YES; - - mNotificationColor = ContrastColorUtil.resolveContrastColor(mContext, - mEntry.getSbn().getNotification().color, - getBackgroundColorWithoutTint(), nightMode); - } - public HybridNotificationView getSingleLineView() { return mPrivateLayout.getSingleLineView(); } @@ -2253,6 +2264,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mEntry = entry; } + @VisibleForTesting + protected void setEntryAdapter(EntryAdapter entry) { + mEntryAdapter = entry; + } + private final Runnable mExpireRecentlyAlertedFlag = () -> applyAudiblyAlertedRecently(false); private void applyAudiblyAlertedRecently(boolean audiblyAlertedRecently) { @@ -2490,7 +2506,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mTranslateableViews.get(i).setTranslationX(0); } invalidateOutline(); - getEntry().getIcons().getShelfIcon().setScrollX(0); + getShelfIcon().setScrollX(0); } if (mMenuRow != null) { @@ -2609,7 +2625,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView // In order to keep the shelf in sync with this swiping, we're simply translating // it's icon by the same amount. The translation is already being used for the normal // positioning, so we can use the scrollX instead. - getEntry().getIcons().getShelfIcon().setScrollX((int) -translationX); + getShelfIcon().setScrollX((int) -translationX); } if (mMenuRow != null && mMenuRow.getMenuView() != null) { @@ -2836,7 +2852,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public @NonNull StatusBarIconView getShelfIcon() { - return getEntry().getIcons().getShelfIcon(); + if (NotificationBundleUi.isEnabled()) { + return getEntryAdapter().getIcons().getShelfIcon(); + } else { + return mEntry.getIcons().getShelfIcon(); + } } @Override @@ -3065,8 +3085,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * except for legacy use cases. */ public boolean canShowHeadsUp() { + boolean canEntryHun = NotificationBundleUi.isEnabled() + ? mEntryAdapter.canPeek() + : mEntry.isStickyAndNotDemoted(); if (mOnKeyguard && !isDozing() && !isBypassEnabled() && - (!mEntry.isStickyAndNotDemoted() + (!canEntryHun || (!mIgnoreLockscreenConstraints && mSaveSpaceOnLockscreen))) { return false; } @@ -3105,7 +3128,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } if (!mIsSummaryWithChildren && wasSummary) { // Reset the 'when' once the row stops being a summary - mPublicLayout.setNotificationWhen(mEntry.getSbn().getNotification().getWhen()); + if (NotificationBundleUi.isEnabled()) { + mPublicLayout.setNotificationWhen(mEntryAdapter.getWhen()); + } else { + mPublicLayout.setNotificationWhen(mEntry.getSbn().getNotification().getWhen()); + } } getShowingLayout().updateBackgroundColor(false /* animate */); mPrivateLayout.updateExpandButtons(isExpandable()); @@ -3374,7 +3401,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView */ @Override public boolean canExpandableViewBeDismissed() { - if (areGutsExposed() || !mEntry.hasFinishedInitialization()) { + if (areGutsExposed() || !hasFinishedInitialization()) { return false; } return canViewBeDismissed(); @@ -3398,7 +3425,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * clearability see {@link NotificationEntry#isClearable()}. */ public boolean canViewBeCleared() { - return mEntry.isClearable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); + if (NotificationBundleUi.isEnabled()) { + return mEntryAdapter.isClearable() + && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); + } else { + return mEntry.isClearable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); + } } private boolean shouldShowPublic() { @@ -4075,6 +4107,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isMediaRow() { + NotificationBundleUi.assertInLegacyMode(); return mEntry.getSbn().getNotification().isMediaNotification(); } @@ -4197,7 +4230,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void dump(PrintWriter pwOriginal, String[] args) { IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); // Skip super call; dump viewState ourselves - pw.println("Notification: " + mEntry.getKey()); + if (NotificationBundleUi.isEnabled()) { + pw.println("Notification: " + mEntryAdapter.getKey()); + } else { + pw.println("Notification: " + mEntry.getKey()); + } DumpUtilsKt.withIncreasedIndent(pw, () -> { pw.println(this); pw.print("visibility: " + getVisibility()); @@ -4228,6 +4265,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView pw.print(", isPinned: " + isPinned()); pw.print(", expandedWhenPinned: " + mExpandedWhenPinned); pw.print(", isMinimized: " + mIsMinimized); + pw.print(", isAboveShelf: " + isAboveShelf()); pw.println(); if (NotificationContentView.INCLUDE_HEIGHTS_TO_DUMP) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index b43a387a5edb..07711b6e0eb8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -374,7 +374,11 @@ public class ExpandableNotificationRowController implements NotifViewController mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { - mView.getEntry().setInitializationTime(mClock.elapsedRealtime()); + if (NotificationBundleUi.isEnabled()) { + mView.setInitializationTime(mClock.elapsedRealtime()); + } else { + mView.getEntry().setInitializationTime(mClock.elapsedRealtime()); + } mPluginManager.addPluginListener(mView, NotificationMenuRowPlugin.class, false /* Allow multiple */); if (!SceneContainerFlag.isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index dd3a9c9dcf21..33c36d8c4c76 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row; +import static com.android.systemui.Flags.notificationRowTransparency; import static com.android.systemui.util.ColorUtilKt.hexColorString; import android.content.Context; @@ -23,6 +24,7 @@ import android.content.res.ColorStateList; import android.graphics.Canvas; import android.graphics.Path; import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; @@ -34,8 +36,10 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.Dumpable; +import com.android.systemui.common.shared.colors.SurfaceEffectColors; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss; import com.android.systemui.util.DrawableDumpKt; @@ -69,6 +73,7 @@ public class NotificationBackgroundView extends View implements Dumpable, private final ColorStateList mLightColoredStatefulColors; private final ColorStateList mDarkColoredStatefulColors; private final int mNormalColor; + private boolean mBgIsColorized = false; private final int convexR = 9; private final int concaveR = 22; @@ -82,8 +87,12 @@ public class NotificationBackgroundView extends View implements Dumpable, R.color.notification_state_color_light); mDarkColoredStatefulColors = getResources().getColorStateList( R.color.notification_state_color_dark); - mNormalColor = mContext.getColor( - com.android.internal.R.color.materialColorSurfaceContainerHigh); + if (notificationRowTransparency()) { + mNormalColor = SurfaceEffectColors.surfaceEffect1(getResources()); + } else { + mNormalColor = mContext.getColor( + com.android.internal.R.color.materialColorSurfaceContainerHigh); + } mFocusOverlayStroke = getResources().getDimension(R.dimen.notification_focus_stroke_width); } @@ -132,6 +141,21 @@ public class NotificationBackgroundView extends View implements Dumpable, } } + /** + * A way to tell whether the background has been colorized. + */ + public boolean isColorized() { + return mBgIsColorized; + } + + /** + * A way to inform this class whether the background has been colorized. + * We need to know this, in order to *not* override that color. + */ + public void setBgIsColorized(boolean b) { + mBgIsColorized = b; + } + private Path calculateDismissButtonCutoutPath(Rect backgroundBounds) { // TODO(b/365585705): Adapt to RTL after the UX design is finalized. @@ -280,7 +304,7 @@ public class NotificationBackgroundView extends View implements Dumpable, setCustomBackground(d); } - private Drawable getBaseBackgroundLayer() { + public Drawable getBaseBackgroundLayer() { return ((LayerDrawable) mBackground).getDrawable(0); } @@ -288,11 +312,27 @@ public class NotificationBackgroundView extends View implements Dumpable, return ((LayerDrawable) mBackground).getDrawable(1); } + private void updateBaseLayerColor() { + // BG base layer being a drawable, there isn't a method like setColor() to color it. + // Instead, we set a color filter that essentially replaces every pixel of the drawable. + // For non-colorized notifications, this function specifies a new color token. + // For colorized notifications, this uses a color that matches the tint color at 90% alpha. + getBaseBackgroundLayer().setColorFilter( + new PorterDuffColorFilter( + isColorized() + ? ColorUtils.setAlphaComponent(mTintColor, (int) (255 * 0.9f)) + : SurfaceEffectColors.surfaceEffect1(getResources()), + PorterDuff.Mode.SRC)); // SRC operator discards the drawable's color+alpha + } + public void setTint(int tintColor) { Drawable baseLayer = getBaseBackgroundLayer(); baseLayer.mutate().setTintMode(PorterDuff.Mode.SRC_ATOP); baseLayer.setTint(tintColor); mTintColor = tintColor; + if (notificationRowTransparency()) { + updateBaseLayerColor(); + } setStatefulColors(); invalidate(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 0c1dd2e026b6..c31f4ad5c3f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -247,7 +247,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder entry.getRanking().getSummarization()); } else { result.mPublicInflatedSingleLineViewModel = - SingleLineViewInflater.inflateRedactedSingleLineViewModel( + SingleLineViewInflater.inflatePublicSingleLineViewModel( row.getContext(), isConversation ); @@ -509,7 +509,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder new Notification.Builder(packageContext, original.getChannelId()); redacted.setContentTitle(original.extras.getCharSequence(Notification.EXTRA_TITLE)); CharSequence redactedMessage = systemUiContext.getString( - R.string.redacted_notification_single_line_text + R.string.redacted_otp_notification_single_line_text ); redacted.setWhen(original.getWhen()); @@ -1362,7 +1362,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder ); } else { result.mPublicInflatedSingleLineViewModel = - SingleLineViewInflater.inflateRedactedSingleLineViewModel( + SingleLineViewInflater.inflatePublicSingleLineViewModel( mContext, isConversation ); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 6e638f5de209..9a75253295d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -73,6 +73,7 @@ import com.android.systemui.statusbar.notification.collection.render.NotifGutsVi import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -436,7 +437,9 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta onNasFeedbackClick, mUiEventLogger, mDeviceProvisionedController.isDeviceProvisioned(), - row.getIsNonblockable(), + NotificationBundleUi.isEnabled() + ? !row.getEntry().isBlockable() + : row.getIsNonblockable(), mHighPriorityProvider.isHighPriority(row.getEntry()), mAssistantFeedbackController, mMetricsLogger, @@ -480,7 +483,9 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta row.getEntry(), onSettingsClick, mDeviceProvisionedController.isDeviceProvisioned(), - row.getIsNonblockable()); + NotificationBundleUi.isEnabled() + ? !row.getEntry().isBlockable() + : row.getIsNonblockable()); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java index b96b224a7d2e..ab382df13d10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java @@ -186,7 +186,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } @Override - public void createMenu(ViewGroup parent, StatusBarNotification sbn) { + public void createMenu(ViewGroup parent) { mParent = (ExpandableNotificationRow) parent; createMenuViews(true /* resetState */); } @@ -227,7 +227,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } @Override - public void onNotificationUpdated(StatusBarNotification sbn) { + public void onNotificationUpdated() { if (mMenuContainer == null) { // Menu hasn't been created yet, no need to do anything. return; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 761d3fe91cd0..b9a3594a007e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -754,7 +754,7 @@ constructor( summarization = null, ) } else { - SingleLineViewInflater.inflateRedactedSingleLineViewModel( + SingleLineViewInflater.inflatePublicSingleLineViewModel( systemUiContext, entry.ranking.isConversation, ) @@ -792,7 +792,7 @@ constructor( val redacted = Notification.Builder(packageContext, original.channelId) redacted.setContentTitle(original.extras.getCharSequence(Notification.EXTRA_TITLE)) val redactedMessage = - sysUiContext.getString(R.string.redacted_notification_single_line_text) + sysUiContext.getString(R.string.redacted_otp_notification_single_line_text) if (originalStyle is MessagingStyle) { val newStyle = MessagingStyle(originalStyle.user) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt index b3c8f2219f4d..ea73b4ba8811 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt @@ -71,7 +71,7 @@ internal object SingleLineViewInflater { var contentText = if (redactText) { systemUiContext.getString( - com.android.systemui.res.R.string.redacted_notification_single_line_text + com.android.systemui.res.R.string.redacted_otp_notification_single_line_text ) } else { HybridGroupManager.resolveText(notification) @@ -120,7 +120,7 @@ internal object SingleLineViewInflater { } @JvmStatic - fun inflateRedactedSingleLineViewModel( + fun inflatePublicSingleLineViewModel( context: Context, isConversation: Boolean = false, ): SingleLineViewModel { @@ -144,7 +144,7 @@ internal object SingleLineViewInflater { com.android.systemui.res.R.string.redacted_notification_single_line_title ), context.getString( - com.android.systemui.res.R.string.redacted_notification_single_line_text + com.android.systemui.res.R.string.public_notification_single_line_text ), conversationData, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCallTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCallTemplateViewWrapper.kt index 2d946945597e..e887e25b74a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCallTemplateViewWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCallTemplateViewWrapper.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row.wrapper +import android.app.Flags.notificationsRedesignTemplates import android.content.Context import android.view.View import com.android.internal.widget.CachingIconView @@ -25,17 +26,13 @@ import com.android.systemui.statusbar.notification.NotificationFadeAware import com.android.systemui.statusbar.notification.NotificationUtils import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow -/** - * Wraps a notification containing a call template - */ -class NotificationCallTemplateViewWrapper constructor( - ctx: Context, - view: View, - row: ExpandableNotificationRow -) : NotificationTemplateViewWrapper(ctx, view, row) { +/** Wraps a notification containing a call template */ +class NotificationCallTemplateViewWrapper +constructor(ctx: Context, view: View, row: ExpandableNotificationRow) : + NotificationTemplateViewWrapper(ctx, view, row) { private val minHeightWithActions: Int = - NotificationUtils.getFontScaledHeight(ctx, R.dimen.notification_max_height) + NotificationUtils.getFontScaledHeight(ctx, R.dimen.notification_max_height) private val callLayout: CallLayout = view as CallLayout private lateinit var conversationIconContainer: View @@ -48,13 +45,17 @@ class NotificationCallTemplateViewWrapper constructor( private fun resolveViews() { with(callLayout) { conversationIconContainer = - requireViewById(com.android.internal.R.id.conversation_icon_container) + requireViewById(com.android.internal.R.id.conversation_icon_container) conversationIconView = requireViewById(com.android.internal.R.id.conversation_icon) conversationBadgeBg = - requireViewById(com.android.internal.R.id.conversation_icon_badge_bg) + requireViewById(com.android.internal.R.id.conversation_icon_badge_bg) expandBtn = requireViewById(com.android.internal.R.id.expand_button) appName = requireViewById(com.android.internal.R.id.app_name_text) - conversationTitleView = requireViewById(com.android.internal.R.id.conversation_text) + conversationTitleView = + requireViewById( + if (notificationsRedesignTemplates()) com.android.internal.R.id.title + else com.android.internal.R.id.conversation_text + ) } } @@ -68,20 +69,12 @@ class NotificationCallTemplateViewWrapper constructor( override fun updateTransformedTypes() { // This also clears the existing types super.updateTransformedTypes() - addTransformedViews( - appName, - conversationTitleView - ) - addViewsTransformingToSimilar( - conversationIconView, - conversationBadgeBg, - expandBtn - ) + addTransformedViews(appName, conversationTitleView) + addViewsTransformingToSimilar(conversationIconView, conversationBadgeBg, expandBtn) } override fun disallowSingleClick(x: Float, y: Float): Boolean { - val isOnExpandButton = expandBtn.visibility == View.VISIBLE && - isOnView(expandBtn, x, y) + val isOnExpandButton = expandBtn.visibility == View.VISIBLE && isOnView(expandBtn, x, y) return isOnExpandButton || super.disallowSingleClick(x, y) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt index 9d13ab53ec71..6a96fba8f28d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt @@ -17,10 +17,12 @@ package com.android.systemui.statusbar.notification.row.wrapper import android.app.Flags +import android.app.Flags.notificationsRedesignTemplates import android.content.Context import android.graphics.drawable.AnimatedImageDrawable import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import com.android.internal.widget.CachingIconView import com.android.internal.widget.ConversationLayout import com.android.internal.widget.MessagingGroup @@ -53,8 +55,8 @@ class NotificationConversationTemplateViewWrapper( private lateinit var badgeIconView: NotificationRowIconView private lateinit var conversationBadgeBg: View private lateinit var expandBtn: View - private lateinit var expandBtnContainer: View - private lateinit var imageMessageContainer: ViewGroup + private var expandBtnContainer: View? = null + private var imageMessageContainer: ViewGroup? = null private lateinit var messageContainers: ArrayList<MessagingGroup> private lateinit var messagingLinearLayout: MessagingLinearLayout private lateinit var conversationTitleView: View @@ -78,10 +80,14 @@ class NotificationConversationTemplateViewWrapper( conversationBadgeBg = requireViewById(com.android.internal.R.id.conversation_icon_badge_bg) expandBtn = requireViewById(com.android.internal.R.id.expand_button) - expandBtnContainer = requireViewById(com.android.internal.R.id.expand_button_container) + expandBtnContainer = findViewById(com.android.internal.R.id.expand_button_container) importanceRing = requireViewById(com.android.internal.R.id.conversation_icon_badge_ring) appName = requireViewById(com.android.internal.R.id.app_name_text) - conversationTitleView = requireViewById(com.android.internal.R.id.conversation_text) + conversationTitleView = + requireViewById( + if (notificationsRedesignTemplates()) com.android.internal.R.id.title + else com.android.internal.R.id.conversation_text + ) facePileTop = findViewById(com.android.internal.R.id.conversation_face_pile_top) facePileBottom = findViewById(com.android.internal.R.id.conversation_face_pile_bottom) facePileBottomBg = @@ -133,11 +139,21 @@ class NotificationConversationTemplateViewWrapper( expandable: Boolean, onClickListener: View.OnClickListener, requestLayout: Boolean, - ) = conversationLayout.updateExpandability(expandable, onClickListener) + ) { + if (notificationsRedesignTemplates()) { + super.updateExpandability(expandable, onClickListener, requestLayout) + } else { + conversationLayout.updateExpandability(expandable, onClickListener) + } + } override fun disallowSingleClick(x: Float, y: Float): Boolean { val isOnExpandButton = - expandBtnContainer.visibility == View.VISIBLE && isOnView(expandBtnContainer, x, y) + if (notificationsRedesignTemplates()) { + expandBtn.isVisible && isOnView(expandBtn, x, y) + } else { + expandBtnContainer?.visibility == View.VISIBLE && isOnView(expandBtnContainer, x, y) + } return isOnExpandButton || super.disallowSingleClick(x, y) } @@ -158,7 +174,8 @@ class NotificationConversationTemplateViewWrapper( // and the top level image message container. val containers = messageContainers.asSequence().map { it.messageContainer } + - sequenceOf(imageMessageContainer) + if (notificationsRedesignTemplates()) emptySequence() + else sequenceOf(imageMessageContainer!!) val drawables = containers .flatMap { it.children } 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 3d60e03d7ca4..3ff18efeae53 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 @@ -6686,7 +6686,7 @@ public class NotificationStackScrollLayout static boolean canChildBeCleared(View v) { if (v instanceof ExpandableNotificationRow row) { - if (row.areGutsExposed() || !row.getEntry().hasFinishedInitialization()) { + if (row.areGutsExposed() || !row.hasFinishedInitialization()) { return false; } return row.canViewBeCleared(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 06b989aaab57..08692bea2292 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -1227,13 +1227,17 @@ public class StackScrollAlgorithm { float baseZ = ambientState.getBaseZHeight(); if (SceneContainerFlag.isEnabled()) { - // SceneContainer flags off this logic, and just sets the baseZ because: + // SceneContainer simplifies this logic, because: // - there are no overlapping HUNs anymore, no need for multiplying their shadows // - shadows for HUNs overlapping with the stack are now set from updateHeadsUpStates - // - shadows for HUNs overlapping with the shelf are NOT set anymore, because it only - // happens on AOD/Pulsing, where they're displayed on a black background so a shadow - // wouldn't be visible. - childViewState.setZTranslation(baseZ); + if (child.isPinned() || ambientState.getTrackedHeadsUpRow() == child) { + // set a default elevation on the HUN, which would be overridden + // from updateHeadsUpStates if it is displayed in the shade + childViewState.setZTranslation(baseZ + mPinnedZTranslationExtra); + } else { + // set baseZ for every notification + childViewState.setZTranslation(baseZ); + } } else { if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible && !ambientState.isDozingAndNotPulsing(child) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 54efa4a2bcf2..34b65603542c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -20,6 +20,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import android.content.Context import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.flow.flowName +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor @@ -44,15 +45,18 @@ import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.AodToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DozingToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel @@ -84,7 +88,9 @@ import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNoti import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf +import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.FlowDumperImpl import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine import com.android.systemui.util.kotlin.sample @@ -135,7 +141,9 @@ constructor( private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel, private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, + private val aodToGlanceableHubTransitionViewModel: AodToGlanceableHubTransitionViewModel, private val aodToPrimaryBouncerTransitionViewModel: AodToPrimaryBouncerTransitionViewModel, + private val dozingToDreamingTransitionViewModel: DozingToDreamingTransitionViewModel, dozingToGlanceableHubTransitionViewModel: DozingToGlanceableHubTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, @@ -144,6 +152,7 @@ constructor( private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, private val glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, + private val glanceableHubToAodTransitionViewModel: GlanceableHubToAodTransitionViewModel, private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel, private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel, private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel, @@ -293,20 +302,40 @@ constructor( .distinctUntilChanged() .dumpWhileCollecting("configurationBasedDimensions") + private val isOnAnyBouncer: Flow<Boolean> = + anyOf( + keyguardTransitionInteractor.transitionValue(ALTERNATE_BOUNCER).map { it > 0f }, + keyguardTransitionInteractor + .transitionValue( + scene = Scenes.Bouncer, + stateWithoutSceneContainer = PRIMARY_BOUNCER, + ) + .map { it > 0f }, + ) + /** If the user is visually on one of the unoccluded lockscreen states. */ val isOnLockscreen: Flow<Boolean> = - anyOf( - keyguardTransitionInteractor.transitionValue(AOD).map { it > 0f }, - keyguardTransitionInteractor.transitionValue(DOZING).map { it > 0f }, - keyguardTransitionInteractor.transitionValue(ALTERNATE_BOUNCER).map { it > 0f }, - keyguardTransitionInteractor - .transitionValue( - scene = Scenes.Bouncer, - stateWithoutSceneContainer = PRIMARY_BOUNCER, - ) - .map { it > 0f }, - keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f }, - ) + if (glanceableHubV2()) { + anyOf( + keyguardTransitionInteractor.transitionValue(AOD).map { it > 0f }, + keyguardTransitionInteractor.transitionValue(DOZING).map { it > 0f }, + keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f }, + allOf( + // Exclude bouncer showing over communal hub, as this should not be + // considered + // "lockscreen" + not(communalSceneInteractor.isCommunalVisible), + isOnAnyBouncer, + ), + ) + } else { + anyOf( + keyguardTransitionInteractor.transitionValue(AOD).map { it > 0f }, + keyguardTransitionInteractor.transitionValue(DOZING).map { it > 0f }, + keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f }, + isOnAnyBouncer, + ) + } .flowName("isOnLockscreen") .stateIn( scope = applicationScope, @@ -571,7 +600,9 @@ constructor( aodToGoneTransitionViewModel.notificationAlpha(viewState), aodToLockscreenTransitionViewModel.notificationAlpha, aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), + aodToGlanceableHubTransitionViewModel.lockscreenAlpha(viewState), aodToPrimaryBouncerTransitionViewModel.notificationAlpha, + dozingToDreamingTransitionViewModel.notificationAlpha, dozingToLockscreenTransitionViewModel.lockscreenAlpha, dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToPrimaryBouncerTransitionViewModel.notificationAlpha, @@ -591,6 +622,7 @@ constructor( offToLockscreenTransitionViewModel.lockscreenAlpha, primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState), glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, + glanceableHubToAodTransitionViewModel.lockscreenAlpha, lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index de7215461c80..36193bd87ce2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -441,6 +441,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mAnimationScheduler.addCallback(mAnimationCallback); mUserInfoController.addCallback(mOnUserInfoChangedListener); mStatusBarStateController.addCallback(mStatusBarStateListener); + mStatusBarState = mStatusBarStateController.getState(); mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); mDisableStateTracker.startTracking(mCommandQueue, mView.getDisplay().getDisplayId()); if (mTintedIconManager == null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 6c8e1825ea0a..ba41fd4c40ab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -65,6 +65,7 @@ import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor; +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationClickNotifier; import com.android.systemui.statusbar.NotificationLockscreenUserManager; @@ -116,6 +117,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final static String TAG = "StatusBarNotificationActivityStarter"; private final Context mContext; + private final ShadeDialogContextInteractor mContextInteractor; private final Handler mMainThreadHandler; private final Executor mUiBgExecutor; @@ -156,6 +158,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Inject StatusBarNotificationActivityStarter( @ShadeDisplayAware Context context, + ShadeDialogContextInteractor contextInteractor, @Main Handler mainThreadHandler, @Background Executor uiBgExecutor, NotificationVisibilityProvider visibilityProvider, @@ -188,6 +191,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit PowerInteractor powerInteractor, UserTracker userTracker) { mContext = context; + mContextInteractor = contextInteractor; mMainThreadHandler = mainThreadHandler; mUiBgExecutor = uiBgExecutor; mVisibilityProvider = visibilityProvider; @@ -491,7 +495,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean animate, boolean isActivityIntent) { mLogger.logStartNotificationIntent(entry); - final int displayId = mContext.getDisplayId(); + final int displayId = mContextInteractor.getContext().getDisplayId(); try { ActivityTransitionAnimator.Controller animationController = new StatusBarTransitionAnimatorController( @@ -532,7 +536,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit public void startNotificationGutsIntent(@NonNull final Intent intent, final int appUid, @NonNull ExpandableNotificationRow row) { boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); - final int displayId = mContext.getDisplayId(); + final int displayId = mContextInteractor.getContext().getDisplayId(); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override public boolean onDismiss() { @@ -571,7 +575,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startHistoryIntent(View view, boolean showHistory) { ModesEmptyShadeFix.assertInLegacyMode(); - final int displayId = mContext.getDisplayId(); + final int displayId = mContextInteractor.getContext().getDisplayId(); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override @@ -621,7 +625,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startSettingsIntent(@NonNull View view, @NonNull SettingsIntent intentInfo) { - final int displayId = mContext.getDisplayId(); + final int displayId = mContextInteractor.getContext().getDisplayId(); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarChipsModernization.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarChipsModernization.kt index 9f8b45578903..b77e8f2ffefc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarChipsModernization.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarChipsModernization.kt @@ -23,6 +23,10 @@ import com.android.systemui.flags.RefactorFlagUtils @Suppress("NOTHING_TO_INLINE") object StatusBarChipsModernization { /** The aconfig flag name */ + @Deprecated( + "For tests, use @EnableChipsModernization or @DisableChipsModernization " + + "annotations instead" + ) const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CHIPS_MODERNIZATION /** A token used for dependency declaration */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index c52536d2b312..10821dffd394 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -21,7 +21,6 @@ import android.telephony.CarrierConfigManager import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING import com.android.settingslib.SignalIcon.MobileIconGroup -import com.android.settingslib.flags.Flags import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -29,11 +28,13 @@ import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository @@ -85,6 +86,12 @@ interface MobileIconsInteractor { /** Whether the mobile icons can be stacked vertically. */ val isStackable: StateFlow<Boolean> + /** + * Observable for the subscriptionId of the current mobile data connection. Null if we don't + * have a valid subscription id + */ + val activeMobileDataSubscriptionId: StateFlow<Int?> + /** True if the active mobile data subscription has data enabled */ val activeDataConnectionHasDataEnabled: StateFlow<Boolean> @@ -168,6 +175,9 @@ constructor( ) .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val activeMobileDataSubscriptionId: StateFlow<Int?> = + mobileConnectionsRepo.activeMobileDataSubscriptionId + override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> = mobileConnectionsRepo.activeMobileDataRepository .flatMapLatest { it?.dataEnabled ?: flowOf(false) } @@ -298,10 +308,16 @@ constructor( .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) override val isStackable = - if (Flags.newStatusBarIcons() && StatusBarRootModernization.isEnabled) { + if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { icons.flatMapLatest { icons -> - combine(icons.map { it.isNonTerrestrial }) { - it.size == 2 && it.none { isNonTerrestrial -> isNonTerrestrial } + combine(icons.map { it.signalLevelIcon }) { signalLevelIcons -> + // These are only stackable if: + // - They are cellular + // - There's exactly two + // - They have the same number of levels + signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let { + it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels + } } } } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 6176a3e9e281..288e49eac5a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -63,6 +63,8 @@ constructor( @VisibleForTesting val reuseCache = ConcurrentHashMap<Int, Pair<MobileIconViewModel, CoroutineScope>>() + val activeMobileDataSubscriptionId: StateFlow<Int?> = interactor.activeMobileDataSubscriptionId + val subscriptionIdsFlow: StateFlow<List<Int>> = interactor.filteredSubscriptions .mapLatest { subscriptions -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt index a2c2a3cd1507..2c85a5150575 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt @@ -25,7 +25,7 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconMod import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -43,8 +43,14 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() initialValue = false, ) - private val iconViewModelFlow: StateFlow<List<MobileIconViewModelCommon>> = - mobileIconsViewModel.mobileSubViewModels + private val iconViewModelFlow: Flow<List<MobileIconViewModelCommon>> = + combine( + mobileIconsViewModel.mobileSubViewModels, + mobileIconsViewModel.activeMobileDataSubscriptionId, + ) { viewModels, activeSubId -> + // Sort to get the active subscription first, if it's set + viewModels.sortedByDescending { it.subscriptionId == activeSubId } + } val dualSim: DualSim? by hydrator.hydratedStateOf( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index cd320a12d577..d7348892356d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -31,6 +31,8 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipBinder +import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipViewBinding +import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModelLegacy import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.core.StatusBarRootModernization @@ -149,6 +151,7 @@ constructor( if (!StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled) { val primaryChipViewBinding = OngoingActivityChipBinder.createBinding(primaryChipView) + launch { viewModel.primaryOngoingActivityChip.collect { primaryChipModel -> OngoingActivityChipBinder.bind( @@ -156,18 +159,14 @@ constructor( primaryChipViewBinding, iconViewStore, ) - if (StatusBarRootModernization.isEnabled) { - when (primaryChipModel) { - is OngoingActivityChipModel.Active -> - primaryChipViewBinding.rootView.show( - shouldAnimateChange = true - ) - is OngoingActivityChipModel.Inactive -> - primaryChipViewBinding.rootView.hide( - state = View.GONE, - shouldAnimateChange = primaryChipModel.shouldAnimate, - ) + if (StatusBarRootModernization.isEnabled) { + launch { + bindLegacyPrimaryOngoingActivityChipWithVisibility( + viewModel, + primaryChipModel, + primaryChipViewBinding, + ) } } else { when (primaryChipModel) { @@ -213,12 +212,14 @@ constructor( ) if (StatusBarRootModernization.isEnabled) { - primaryChipViewBinding.rootView.adjustVisibility( - chips.primary.toVisibilityModel() - ) - secondaryChipViewBinding.rootView.adjustVisibility( - chips.secondary.toVisibilityModel() - ) + launch { + bindOngoingActivityChipsWithVisibility( + viewModel, + chips, + primaryChipViewBinding, + secondaryChipViewBinding, + ) + } } else { listener?.onOngoingActivityStatusChanged( hasPrimaryOngoingActivity = @@ -312,6 +313,52 @@ constructor( } } + /** Bind the (legacy) single primary ongoing activity chip with the status bar visibility */ + private suspend fun bindLegacyPrimaryOngoingActivityChipWithVisibility( + viewModel: HomeStatusBarViewModel, + primaryChipModel: OngoingActivityChipModel, + primaryChipViewBinding: OngoingActivityChipViewBinding, + ) { + viewModel.canShowOngoingActivityChips.collectLatest { visible -> + if (!visible) { + primaryChipViewBinding.rootView.hide(shouldAnimateChange = false) + } else { + when (primaryChipModel) { + is OngoingActivityChipModel.Active -> { + primaryChipViewBinding.rootView.show(shouldAnimateChange = true) + } + + is OngoingActivityChipModel.Inactive -> { + primaryChipViewBinding.rootView.hide( + state = View.GONE, + shouldAnimateChange = primaryChipModel.shouldAnimate, + ) + } + } + } + } + } + + /** Bind the primary/secondary chips along with the home status bar's visibility */ + private suspend fun bindOngoingActivityChipsWithVisibility( + viewModel: HomeStatusBarViewModel, + chips: MultipleOngoingActivityChipsModelLegacy, + primaryChipViewBinding: OngoingActivityChipViewBinding, + secondaryChipViewBinding: OngoingActivityChipViewBinding, + ) { + viewModel.canShowOngoingActivityChips.collectLatest { canShow -> + if (!canShow) { + primaryChipViewBinding.rootView.hide(shouldAnimateChange = false) + secondaryChipViewBinding.rootView.hide(shouldAnimateChange = false) + } else { + primaryChipViewBinding.rootView.adjustVisibility(chips.primary.toVisibilityModel()) + secondaryChipViewBinding.rootView.adjustVisibility( + chips.secondary.toVisibilityModel() + ) + } + } + } + private fun SystemEventAnimationState.isAnimatingChip() = when (this) { AnimatingIn, @@ -374,43 +421,42 @@ constructor( if (visibility == View.INVISIBLE || visibility == View.GONE) { return } + alpha = 0f visibility = state } // See CollapsedStatusBarFragment#hide. private fun View.hide(state: Int = View.INVISIBLE, shouldAnimateChange: Boolean) { + animate().cancel() if (visibility == View.INVISIBLE || visibility == View.GONE) { return } - val v = this - v.animate().cancel() if (!shouldAnimateChange) { - v.alpha = 0f - v.visibility = state + alpha = 0f + visibility = state return } - v.animate() + animate() .alpha(0f) .setDuration(CollapsedStatusBarFragment.FADE_OUT_DURATION.toLong()) .setStartDelay(0) .setInterpolator(Interpolators.ALPHA_OUT) - .withEndAction { v.visibility = state } + .withEndAction { visibility = state } } // See CollapsedStatusBarFragment#show. private fun View.show(shouldAnimateChange: Boolean) { - if (visibility == View.VISIBLE) { + animate().cancel() + if (visibility == View.VISIBLE && alpha >= 1f) { return } - val v = this - v.animate().cancel() - v.visibility = View.VISIBLE + visibility = View.VISIBLE if (!shouldAnimateChange) { - v.alpha = 1f + alpha = 1f return } - v.animate() + animate() .alpha(1f) .setDuration(CollapsedStatusBarFragment.FADE_IN_DURATION.toLong()) .setInterpolator(Interpolators.ALPHA_IN) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt index 465a43fbfb9e..a51da0e4cc73 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt @@ -17,9 +17,12 @@ package com.android.systemui.statusbar.pipeline.shared.ui.composable import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -27,23 +30,28 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius 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.drawscope.DrawScope import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.android.compose.modifiers.height -import com.android.compose.modifiers.width import com.android.systemui.common.ui.compose.Icon import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarBaseHeightFiveBarsSp import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarBaseHeightFourBarsSp import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarsLevelIncrementSp import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarsVerticalPaddingSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.ExclamationCutoutRadiusSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.ExclamationDiameterSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.ExclamationHeightSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.ExclamationHorizontalOffset +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.ExclamationVerticalSpacing import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.HorizontalPaddingFiveBarsSp import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.HorizontalPaddingFourBarsSp import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconHeightSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconPaddingSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconSpacingSp import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconWidthFiveBarsSp import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconWidthFourBarsSp import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.SecondaryBarHeightSp @@ -58,15 +66,16 @@ fun StackedMobileIcon(viewModel: StackedMobileIconViewModel, modifier: Modifier val dualSim = viewModel.dualSim ?: return val contentColor = LocalContentColor.current - - Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { + val padding = with(LocalDensity.current) { IconPaddingSp.toDp() } + val horizontalArrangement = with(LocalDensity.current) { spacedBy(IconSpacingSp.toDp()) } + + Row( + horizontalArrangement = horizontalArrangement, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding(horizontal = padding), + ) { viewModel.networkTypeIcon?.let { - Icon( - it, - tint = contentColor, - modifier = - Modifier.height { IconHeightSp.roundToPx() }.padding(start = 1.dp, end = 2.dp), - ) + Icon(it, tint = contentColor, modifier = Modifier.fillMaxHeight()) } StackedMobileIcon(dualSim, contentColor) @@ -79,23 +88,23 @@ private fun StackedMobileIcon( color: Color, modifier: Modifier = Modifier, ) { - val maxNumberOfLevels = - max(viewModel.primary.numberOfLevels, viewModel.secondary.numberOfLevels) - val dimensions = if (maxNumberOfLevels == 6) FiveBarsDimensions else FourBarsDimensions + // Removing 1 to get the real number of bars + val numberOfBars = max(viewModel.primary.numberOfLevels, viewModel.secondary.numberOfLevels) - 1 + val dimensions = if (numberOfBars == 5) FiveBarsDimensions else FourBarsDimensions val iconSize = with(LocalDensity.current) { dimensions.totalWidth.toDp() to IconHeightSp.toDp() } - Canvas(modifier.size(width = iconSize.first, height = iconSize.second)) { + Canvas(modifier.width(iconSize.first).height(iconSize.second)) { val verticalPaddingPx = BarsVerticalPaddingSp.roundToPx() val horizontalPaddingPx = dimensions.barsHorizontalPadding.roundToPx() - val totalPaddingWidthPx = horizontalPaddingPx * (maxNumberOfLevels - 1) + val totalPaddingWidthPx = horizontalPaddingPx * (numberOfBars - 1) - val barWidthPx = (size.width - totalPaddingWidthPx) / maxNumberOfLevels + val barWidthPx = (size.width - totalPaddingWidthPx) / numberOfBars val dotHeightPx = SecondaryBarHeightSp.toPx() val baseBarHeightPx = dimensions.barBaseHeight.toPx() var xOffsetPx = 0f - for (bar in 1..maxNumberOfLevels) { + for (bar in 1..numberOfBars) { // Bottom dots representing secondary sim val dotYOffsetPx = size.height - dotHeightPx if (bar <= viewModel.secondary.numberOfLevels) { @@ -123,6 +132,10 @@ private fun StackedMobileIcon( xOffsetPx += barWidthPx + horizontalPaddingPx } + + if (viewModel.primary.showExclamationMark) { + drawExclamationCutout(color) + } } } @@ -143,6 +156,39 @@ private fun DrawScope.drawMobileIconBar( ) } +private fun DrawScope.drawExclamationCutout(color: Color) { + // Exclamation mark is bottom aligned with the canvas + val exclamationDiameterPx = ExclamationDiameterSp.toPx() + val exclamationRadiusPx = ExclamationDiameterSp.toPx() / 2 + val exclamationTotalHeight = + ExclamationHeightSp.toPx() + ExclamationVerticalSpacing.toPx() + exclamationDiameterPx + val exclamationDotCenter = + Offset(size.width - ExclamationHorizontalOffset.toPx(), size.height - exclamationRadiusPx) + val exclamationMarkTopLeft = + Offset(exclamationDotCenter.x - exclamationRadiusPx, size.height - exclamationTotalHeight) + val exclamationCornerRadius = CornerRadius(exclamationRadiusPx) + val cutoutCenter = Offset(exclamationDotCenter.x, size.height - (exclamationTotalHeight / 2)) + + // Transparent cutout + drawCircle( + color = Color.Transparent, + radius = ExclamationCutoutRadiusSp.toPx(), + center = cutoutCenter, + blendMode = BlendMode.SrcIn, + ) + + // Top bar for the exclamation mark + drawRoundRect( + color = color, + topLeft = exclamationMarkTopLeft, + size = Size(exclamationDiameterPx, ExclamationHeightSp.toPx()), + cornerRadius = exclamationCornerRadius, + ) + + // Bottom circle for the exclamation mark + drawCircle(color = color, center = exclamationDotCenter, radius = exclamationRadiusPx) +} + private abstract class BarsDependentDimensions( val totalWidth: TextUnit, val barsHorizontalPadding: TextUnit, @@ -166,10 +212,19 @@ private object FiveBarsDimensions : private object StackedMobileIconDimensions { // Common dimensions val IconHeightSp = 12.sp + val IconPaddingSp = 4.sp + val IconSpacingSp = 2.sp val BarsVerticalPaddingSp = 1.5.sp val BarsLevelIncrementSp = 1.sp val SecondaryBarHeightSp = 3.sp + // Exclamation cutout dimensions + val ExclamationCutoutRadiusSp = 5.sp + val ExclamationDiameterSp = 1.5.sp + val ExclamationHeightSp = 4.5.sp + val ExclamationVerticalSpacing = 1.sp + val ExclamationHorizontalOffset = 1.sp + // Dimensions dependant on the number of total bars val IconWidthFiveBarsSp = 18.5.sp val IconWidthFourBarsSp = 16.sp diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileModel.kt index d3e37119fdcb..2433d112bc69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.Text.Companion.loadText import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon /** Model describing the state that the QS Internet tile should be in. */ sealed interface InternetTileModel { @@ -49,11 +50,14 @@ sealed interface InternetTileModel { state.contentDescription = contentDescription.loadContentDescription(context) // To support both SignalDrawable and other icons, give priority to icons over IDs - if (icon != null) { - state.icon = icon - } else if (iconId != null) { - state.icon = QSTileImpl.maybeLoadResourceIcon(iconId!!, context) - } + state.icon = + when { + icon is ResourceIcon -> + QSTileImpl.maybeLoadResourceIcon((icon as ResourceIcon).resId, context) + icon != null -> icon + iconId != null -> QSTileImpl.maybeLoadResourceIcon(iconId!!, context) + else -> null + } state.state = if (this is Active) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index 9ae2cb203d91..807e90567eb7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -149,6 +149,9 @@ interface HomeStatusBarViewModel : Activatable { */ val isHomeStatusBarAllowedByScene: StateFlow<Boolean> + /** True if the home status bar is showing, and there is no HUN happening */ + val canShowOngoingActivityChips: Flow<Boolean> + /** True if the operator name view is not hidden due to HUN or other visibility state */ val shouldShowOperatorNameView: Flow<Boolean> val isClockVisible: Flow<VisibilityModel> @@ -412,6 +415,15 @@ constructor( ) .flowOn(bgDispatcher) + override val canShowOngoingActivityChips: Flow<Boolean> = + combine( + isHomeStatusBarAllowed, + keyguardInteractor.isSecureCameraActive, + headsUpNotificationInteractor.statusBarHeadsUpStatus, + ) { isHomeStatusBarAllowed, isSecureCameraActive, headsUpState -> + isHomeStatusBarAllowed && !isSecureCameraActive && !headsUpState.isPinned + } + override val isClockVisible: Flow<VisibilityModel> = combine( shouldHomeStatusBarBeVisible, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java index 88cf46a0ca07..b13e01be40f7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java @@ -29,7 +29,6 @@ import androidx.annotation.NonNull; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; import com.android.systemui.Dumpable; -import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.util.wrapper.RotationPolicyWrapper; @@ -43,7 +42,6 @@ import javax.inject.Inject; * Handles reading and writing of rotation lock settings per device state, as well as setting the * rotation lock when device state changes. */ -@SysUISingleton public final class DeviceStateRotationLockSettingController implements Listenable, RotationLockController.RotationLockControllerCallback, Dumpable { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java index 797aa1f3a3dd..3ee7f33e3d3a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java @@ -27,8 +27,10 @@ import androidx.annotation.NonNull; import com.android.internal.view.RotationPolicy.RotationPolicyListener; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.rotationlock.DeviceStateAutoRotateModule.BoundsDeviceStateAutoRotateModule; import com.android.systemui.util.wrapper.RotationPolicyWrapper; +import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; import javax.inject.Inject; @@ -50,21 +52,25 @@ public final class RotationLockControllerImpl implements RotationLockController }; private final RotationPolicyWrapper mRotationPolicy; - private final DeviceStateRotationLockSettingController + private final Optional<DeviceStateRotationLockSettingController> mDeviceStateRotationLockSettingController; private final boolean mIsPerDeviceStateRotationLockEnabled; @Inject public RotationLockControllerImpl( RotationPolicyWrapper rotationPolicyWrapper, - DeviceStateRotationLockSettingController deviceStateRotationLockSettingController, + Optional<DeviceStateRotationLockSettingController> + deviceStateRotationLockSettingController, @Named(DEVICE_STATE_ROTATION_LOCK_DEFAULTS) String[] deviceStateRotationLockDefaults ) { mRotationPolicy = rotationPolicyWrapper; - mDeviceStateRotationLockSettingController = deviceStateRotationLockSettingController; mIsPerDeviceStateRotationLockEnabled = deviceStateRotationLockDefaults.length > 0; - if (mIsPerDeviceStateRotationLockEnabled) { - mCallbacks.add(mDeviceStateRotationLockSettingController); + mDeviceStateRotationLockSettingController = + deviceStateRotationLockSettingController; + + if (mIsPerDeviceStateRotationLockEnabled + && mDeviceStateRotationLockSettingController.isPresent()) { + mCallbacks.add(mDeviceStateRotationLockSettingController.get()); } setListening(true); @@ -113,8 +119,9 @@ public final class RotationLockControllerImpl implements RotationLockController } else { mRotationPolicy.unregisterRotationPolicyListener(mRotationPolicyListener); } - if (mIsPerDeviceStateRotationLockEnabled) { - mDeviceStateRotationLockSettingController.setListening(listening); + if (mIsPerDeviceStateRotationLockEnabled + && mDeviceStateRotationLockSettingController.isPresent()) { + mDeviceStateRotationLockSettingController.get().setListening(listening); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogEventLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogEventLogger.kt index 33ed419afef2..5dbe9b145bc7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogEventLogger.kt @@ -21,29 +21,25 @@ import com.android.settingslib.notification.modes.ZenMode import com.android.systemui.qs.QSModesEvent import javax.inject.Inject -class ModesDialogEventLogger -@Inject -constructor( - private val uiEventLogger: UiEventLogger, -) { +class ModesDialogEventLogger @Inject constructor(private val uiEventLogger: UiEventLogger) { fun logModeOn(mode: ZenMode) { val id = if (mode.isManualDnd) QSModesEvent.QS_MODES_DND_ON else QSModesEvent.QS_MODES_MODE_ON - uiEventLogger.log(id, /* uid= */ 0, mode.rule.packageName) + uiEventLogger.log(id, /* uid= */ 0, mode.ownerPackage) } fun logModeOff(mode: ZenMode) { val id = if (mode.isManualDnd) QSModesEvent.QS_MODES_DND_OFF else QSModesEvent.QS_MODES_MODE_OFF - uiEventLogger.log(id, /* uid= */ 0, mode.rule.packageName) + uiEventLogger.log(id, /* uid= */ 0, mode.ownerPackage) } fun logModeSettings(mode: ZenMode) { val id = if (mode.isManualDnd) QSModesEvent.QS_MODES_DND_SETTINGS else QSModesEvent.QS_MODES_MODE_SETTINGS - uiEventLogger.log(id, /* uid= */ 0, mode.rule.packageName) + uiEventLogger.log(id, /* uid= */ 0, mode.ownerPackage) } fun logOpenDurationDialog(mode: ZenMode) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt index 07f1c3470c83..dc07202dc486 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt @@ -71,10 +71,10 @@ constructor( mode.id in prevIds -> true // Mode is enabled -> show if active (so user can toggle off), or if it // can be manually toggled on - mode.rule.isEnabled -> mode.isActive || mode.rule.isManualInvocationAllowed + mode.isEnabled -> mode.isActive || mode.isManualInvocationAllowed // Mode was created as disabled, or disabled by the app that owns it -> // will be shown with a "Not set" text - !mode.rule.isEnabled -> mode.status == ZenMode.Status.DISABLED_BY_OTHER + !mode.isEnabled -> mode.status == ZenMode.Status.DISABLED_BY_OTHER else -> false } } @@ -97,13 +97,13 @@ constructor( if (mode.isActive) R.string.zen_mode_on else R.string.zen_mode_off ), onClick = { - if (!mode.rule.isEnabled) { + if (!mode.isEnabled) { openSettings(mode) } else if (mode.isActive) { dialogEventLogger.logModeOff(mode) zenModeInteractor.deactivateMode(mode) } else { - if (mode.rule.isManualInvocationAllowed) { + if (mode.isManualInvocationAllowed) { if (zenModeInteractor.shouldAskForZenDuration(mode)) { dialogEventLogger.logOpenDurationDialog(mode) // NOTE: The dialog handles turning on the mode itself. @@ -144,10 +144,10 @@ constructor( * readers, and for the tile subtext will be augmented with the current status of the mode. */ private fun getModeDescription(mode: ZenMode, forAccessibility: Boolean): String? { - if (!mode.rule.isEnabled) { + if (!mode.isEnabled) { return context.resources.getString(R.string.zen_mode_set_up) } - if (!mode.rule.isManualInvocationAllowed && !mode.isActive) { + if (!mode.isManualInvocationAllowed && !mode.isActive) { return context.resources.getString(R.string.zen_mode_no_manual_invocation) } return if (forAccessibility) diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java index 32f2ca6fb696..367f54cf4936 100644 --- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java +++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java @@ -147,7 +147,7 @@ public class CreateUserActivity extends Activity { super.onDestroy(); } - private void addUserNow(String userName, Drawable userIcon, Boolean isAdmin) { + private void addUserNow(String userName, Drawable userIcon, String iconPath, Boolean isAdmin) { mSetupUserDialog.dismiss(); userName = (userName == null || userName.trim().isEmpty()) ? getString(com.android.settingslib.R.string.user_new_user_name) diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt index ef29a387e06f..3bd8af690763 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt @@ -18,12 +18,20 @@ package com.android.systemui.user.domain.interactor import android.os.UserHandle import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.user.data.repository.UserRepository import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn @SysUISingleton -class UserLockedInteractor @Inject constructor(val userRepository: UserRepository) { +class UserLockedInteractor +@Inject +constructor( + @Background val backgroundDispatcher: CoroutineDispatcher, + val userRepository: UserRepository, +) { fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> = - userRepository.isUserUnlocked(userHandle) + userRepository.isUserUnlocked(userHandle).flowOn(backgroundDispatcher) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt index a0be02f1ef7e..bd4c5f50eee7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt @@ -43,6 +43,7 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGri import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.model.TileCategory +import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test @@ -85,7 +86,9 @@ class ResizingTest : SysuiTestCase() { composeRule .onNodeWithContentDescription("tileA") - .performCustomAccessibilityActionWithLabel("Toggle size") + .performCustomAccessibilityActionWithLabel( + context.getString(R.string.accessibility_qs_edit_toggle_tile_size_action) + ) assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(2) } @@ -101,7 +104,9 @@ class ResizingTest : SysuiTestCase() { composeRule .onNodeWithContentDescription("tileD_large") - .performCustomAccessibilityActionWithLabel("Toggle size") + .performCustomAccessibilityActionWithLabel( + context.getString(R.string.accessibility_qs_edit_toggle_tile_size_action) + ) assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index 4ff09d3bc6af..e8b50d580c33 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -45,6 +45,9 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.ArrayMap; @@ -74,10 +77,14 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable; import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -127,6 +134,9 @@ public class ShadeListBuilderTest extends SysuiTestCase { private TestableStabilityManager mStabilityManager; private TestableNotifFilter mFinalizeFilter; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private Map<String, Integer> mNextIdMap = new ArrayMap<>(); private int mNextRank = 0; @@ -561,6 +571,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) public void testFilter_resetsInitalizationTime() { // GIVEN a NotifFilter that filters out a specific package NotifFilter filter1 = spy(new PackageFilter(PACKAGE_1)); @@ -584,6 +595,31 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void testFilter_resetsInitializationTime_onRow() throws Exception { + // GIVEN a NotifFilter that filters out a specific package + NotifFilter filter1 = spy(new PackageFilter(PACKAGE_1)); + mListBuilder.addFinalizeFilter(filter1); + + // GIVEN a notification that was initialized 1 second ago that will be filtered out + final NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(PACKAGE_1) + .setId(nextId(PACKAGE_1)) + .setRank(nextRank()) + .build(); + entry.setRow(new NotificationTestHelper(mContext, mDependency).createRow()); + entry.getRow().setInitializationTime(SystemClock.elapsedRealtime() - 1000); + assertTrue(entry.getRow().hasFinishedInitialization()); + + // WHEN the pipeline is kicked off + mReadyForBuildListener.onBuildList(singletonList(entry), "test"); + mPipelineChoreographer.runIfScheduled(); + + // THEN the entry's initialization time is reset + assertFalse(entry.getRow().hasFinishedInitialization()); + } + + @Test public void testNotifFiltersCanBePreempted() { // GIVEN two notif filters NotifFilter filter1 = spy(new PackageFilter(PACKAGE_2)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 1b5353127f25..24d8d1cc8fae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -75,6 +75,7 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -589,6 +590,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) public void testGetIsNonblockable() throws Exception { ExpandableNotificationRow row = mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt index 47238fedee4d..c874bc6056c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt @@ -36,6 +36,7 @@ import com.android.internal.widget.NotificationActionListLayout import com.android.internal.widget.NotificationExpandButton import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.FeedbackIcon +import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.shared.NotificationBundleUi @@ -82,6 +83,7 @@ class NotificationContentViewTest : SysuiTestCase() { fakeParent = spy(FrameLayout(mContext, /* attrs= */ null).also { it.visibility = View.GONE }) val mockEntry = createMockNotificationEntry() + val mockEntryAdapter = createMockNotificationEntryAdapter() row = spy( when (NotificationBundleUi.isEnabled) { @@ -92,6 +94,7 @@ class NotificationContentViewTest : SysuiTestCase() { UserHandle.CURRENT ).apply { entry = mockEntry + entryAdapter = mockEntryAdapter } } false -> { @@ -611,6 +614,7 @@ class NotificationContentViewTest : SysuiTestCase() { whenever(this.entry).thenReturn(notificationEntry) whenever(this.context).thenReturn(mContext) whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {}) + whenever(this.entryAdapter).thenReturn(createMockNotificationEntryAdapter()) } private fun createMockNotificationEntry() = @@ -624,6 +628,9 @@ class NotificationContentViewTest : SysuiTestCase() { whenever(sbnMock.user).thenReturn(userMock) } + private fun createMockNotificationEntryAdapter() = + mock<EntryAdapter>() + private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout { val outerLayout = LinearLayout(mContext) val innerLayout = LinearLayout(mContext) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt index 3d4c90140adb..99b99ee1860b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt @@ -427,7 +427,6 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .setImportance(NotificationManager.IMPORTANCE_HIGH) .build() - whenever(row.getIsNonblockable()).thenReturn(false) whenever(highPriorityProvider.isHighPriority(entry)).thenReturn(true) val statusBarNotification = entry.sbn gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -463,7 +462,6 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .build() - whenever(row.getIsNonblockable()).thenReturn(false) val statusBarNotification = row.entry.sbn val entry = row.entry gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -499,7 +497,6 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .build() - whenever(row.getIsNonblockable()).thenReturn(false) val statusBarNotification = row.entry.sbn val entry = row.entry gutsManager.initializeNotificationInfo(row, notificationInfoView) @@ -566,7 +563,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { ): NotificationMenuRowPlugin.MenuItem { val menuRow: NotificationMenuRowPlugin = NotificationMenuRow(mContext, peopleNotificationIdentifier) - menuRow.createMenu(row, row!!.entry.sbn) + menuRow.createMenu(row) val menuItem = menuRow.getLongpressMenuItem(mContext) Assert.assertNotNull(menuItem) return menuItem diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 6ac20d40f2dc..955de273c426 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -675,7 +675,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Test public void testClearNotifications_clearAllInProgress() { ExpandableNotificationRow row = createClearableRow(); - when(row.getEntry().hasFinishedInitialization()).thenReturn(true); + when(row.hasFinishedInitialization()).thenReturn(true); doReturn(true).when(mStackScroller).isVisible(row); mStackScroller.addContainerView(row); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index 3190d3ae8f16..28eafa937097 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -76,6 +76,7 @@ import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeControllerImpl; import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.shade.data.repository.ShadeAnimationRepository; +import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor; import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLegacyImpl; import com.android.systemui.statusbar.CommandQueue; @@ -171,6 +172,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { private final FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock()); private ExpandableNotificationRow mNotificationRow; private ExpandableNotificationRow mBubbleNotificationRow; + private FakeShadeDialogContextInteractor mContextInteractor; private final Answer<Void> mCallOnDismiss = answerVoid( (OnDismissAction dismissAction, Runnable cancel, @@ -187,6 +189,8 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mDependency, TestableLooper.get(this)); + mContextInteractor = new FakeShadeDialogContextInteractor(mContext); + // Create standard notification with contentIntent mNotificationRow = notificationTestHelper.createRow(); StatusBarNotification sbn = mNotificationRow.getEntry().getSbn(); @@ -199,10 +203,6 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { bubbleSbn.getNotification().contentIntent = mContentIntent; bubbleSbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL; -// ArrayList<NotificationEntry> activeNotifications = new ArrayList<>(); -// activeNotifications.add(mNotificationRow.getEntry()); -// activeNotifications.add(mBubbleNotificationRow.getEntry()); -// when(mEntryManager.getVisibleNotifications()).thenReturn(activeNotifications); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); when(mOnUserInteractionCallback.registerFutureDismissal(eq(mNotificationRow.getEntry()), anyInt())).thenReturn(mFutureDismissalRunnable); @@ -232,6 +232,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mNotificationActivityStarter = new StatusBarNotificationActivityStarter( getContext(), + mContextInteractor, mHandler, mUiBgExecutor, mVisibilityProvider, diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt index a1122c3cbcd2..a553b176c34a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt @@ -18,10 +18,12 @@ package com.android.systemui.unfold import android.hardware.devicestate.DeviceStateManager import android.hardware.devicestate.DeviceStateManager.FoldStateListener +import android.platform.test.annotations.DisableFlags import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.util.LatencyTracker +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.foldedDeviceStateList import com.android.systemui.halfFoldedDeviceState @@ -46,6 +48,7 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest +@DisableFlags(Flags.FLAG_UNFOLD_LATENCY_TRACKING_FIX) class UnfoldLatencyTrackerTest : SysuiTestCase() { @Mock lateinit var latencyTracker: LatencyTracker 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 0f21a16147f0..b8b2ec5a58ae 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 @@ -16,6 +16,7 @@ package com.android.systemui.communal.domain.interactor +import android.content.pm.UserInfo import android.content.testableContext import android.os.userManager import com.android.systemui.broadcast.broadcastDispatcher @@ -85,27 +86,28 @@ fun Kosmos.setCommunalV2ConfigEnabled(enabled: Boolean) { ) } -suspend fun Kosmos.setCommunalEnabled(enabled: Boolean) { +suspend fun Kosmos.setCommunalEnabled(enabled: Boolean): UserInfo { fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, enabled) - if (enabled) { + return if (enabled) { fakeUserRepository.asMainUser() } else { fakeUserRepository.asDefaultUser() } } -suspend fun Kosmos.setCommunalV2Enabled(enabled: Boolean) { +suspend fun Kosmos.setCommunalV2Enabled(enabled: Boolean): UserInfo { setCommunalV2ConfigEnabled(enabled) - setCommunalEnabled(enabled) + return setCommunalEnabled(enabled) } -suspend fun Kosmos.setCommunalAvailable(available: Boolean) { - setCommunalEnabled(available) +suspend fun Kosmos.setCommunalAvailable(available: Boolean): UserInfo { + val user = setCommunalEnabled(available) fakeKeyguardRepository.setKeyguardShowing(available) fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, available) + return user } -suspend fun Kosmos.setCommunalV2Available(available: Boolean) { +suspend fun Kosmos.setCommunalV2Available(available: Boolean): UserInfo { setCommunalV2ConfigEnabled(available) - setCommunalAvailable(available) + return setCommunalAvailable(available) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt index 93a59eb8ca0c..bdfa875f5429 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt @@ -16,6 +16,9 @@ package com.android.systemui.keyguard.domain.interactor +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.deviceentry.data.repository.deviceEntryRepository import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos @@ -38,5 +41,8 @@ val Kosmos.fromAodTransitionInteractor by keyguardOcclusionInteractor = keyguardOcclusionInteractor, deviceEntryRepository = deviceEntryRepository, wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, + communalSettingsInteractor = communalSettingsInteractor, + communalSceneInteractor = communalSceneInteractor, + communalInteractor = communalInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGlanceableHubTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGlanceableHubTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..1eeecd4b7520 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToGlanceableHubTransitionViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 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.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.glanceableHubBlurComponentFactory +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos + +val Kosmos.aodToGlanceableHubTransitionViewModel by + Kosmos.Fixture { + AodToGlanceableHubTransitionViewModel( + animationFlow = keyguardTransitionAnimationFlow, + blurFactory = glanceableHubBlurComponentFactory, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt index bd0045501ec8..2797b4409ff0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt @@ -47,5 +47,6 @@ val Kosmos.deviceEntryBackgroundViewModel by Fixture { primaryBouncerToLockscreenTransitionViewModel = primaryBouncerToLockscreenTransitionViewModel, lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel, + glanceableHubToAodTransitionViewModel = glanceableHubToAodTransitionViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToAodTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..6004c7f2caec --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToAodTransitionViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 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.keyguard.ui.viewmodel + +import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos + +val Kosmos.glanceableHubToAodTransitionViewModel by + Kosmos.Fixture { + GlanceableHubToAodTransitionViewModel( + deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor, + animationFlow = keyguardTransitionAnimationFlow, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index 78356318cbb4..27ca0f867855 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -94,5 +94,7 @@ val Kosmos.keyguardRootViewModel by Fixture { shadeInteractor = shadeInteractor, wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor, dumpManager = dumpManager, + glanceableHubToAodTransitionViewModel = glanceableHubToAodTransitionViewModel, + aodToGlanceableHubTransitionViewModel = aodToGlanceableHubTransitionViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt index 8d4db8b74061..8a6f68c5da68 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.domain.interactor +import com.android.internal.logging.uiEventLoggerFake import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.log.core.FakeLogBuffer @@ -29,6 +30,7 @@ val Kosmos.iconTilesInteractor by defaultLargeTilesRepository, currentTilesInteractor, qsPreferencesInteractor, + uiEventLoggerFake, largeTileSpanRepository, FakeLogBuffer.Factory.create(), applicationCoroutineScope, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt index 31be050adc98..4f662e0bbab2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt @@ -18,28 +18,37 @@ package com.android.systemui.qs.ui.viewmodel import com.android.systemui.brightness.ui.viewmodel.brightnessSliderViewModelFactory import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor +import com.android.systemui.media.controls.ui.controller.mediaCarouselController +import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.qs.panels.ui.viewmodel.detailsViewModel import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel import com.android.systemui.qs.panels.ui.viewmodel.tileGridViewModelFactory import com.android.systemui.qs.panels.ui.viewmodel.toolbar.toolbarViewModelFactory import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory +import org.mockito.kotlin.mock val Kosmos.quickSettingsContainerViewModelFactory by Kosmos.Fixture { object : QuickSettingsContainerViewModel.Factory { override fun create( - supportsBrightnessMirroring: Boolean + supportsBrightnessMirroring: Boolean, + expansion: Float?, ): QuickSettingsContainerViewModel { return QuickSettingsContainerViewModel( - brightnessSliderViewModelFactory, - shadeHeaderViewModelFactory, - tileGridViewModelFactory, - supportsBrightnessMirroring, - editModeViewModel, - detailsViewModel, - toolbarViewModelFactory, - shadeModeInteractor, + brightnessSliderViewModelFactory = brightnessSliderViewModelFactory, + shadeHeaderViewModelFactory = shadeHeaderViewModelFactory, + tileGridViewModelFactory = tileGridViewModelFactory, + supportsBrightnessMirroring = supportsBrightnessMirroring, + expansion = expansion, + editModeViewModel = editModeViewModel, + detailsViewModel = detailsViewModel, + toolbarViewModelFactory = toolbarViewModelFactory, + shadeModeInteractor = shadeModeInteractor, + mediaCarouselInteractor = mediaCarouselInteractor, + mediaCarouselController = mediaCarouselController, + mediaHost = mock<MediaHost>(), ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt index 6fd7cf6edbe4..2ea2119970ad 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt @@ -16,14 +16,18 @@ package com.android.systemui.shade.domain.interactor +import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter import com.android.systemui.shade.data.repository.shadeHeaderClockRepository +import com.android.systemui.util.time.systemClock var Kosmos.shadeHeaderClockInteractor: ShadeHeaderClockInteractor by Kosmos.Fixture { ShadeHeaderClockInteractor( repository = shadeHeaderClockRepository, activityStarter = activityStarter, + broadcastDispatcher = broadcastDispatcher, + systemClock = systemClock, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt index 08de73be1128..34e5bfde43c9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt @@ -18,7 +18,6 @@ package com.android.systemui.shade.ui.viewmodel import android.content.applicationContext import com.android.systemui.battery.batteryMeterViewControllerFactory -import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -26,7 +25,6 @@ import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeModeInteractor -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.tintedIconManagerFactory import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.mobileIconsInteractor @@ -48,9 +46,6 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by tintedIconManagerFactory = tintedIconManagerFactory, batteryMeterViewControllerFactory = batteryMeterViewControllerFactory, statusBarIconController = mock<StatusBarIconController>(), - notificationIconContainerStatusBarViewBinder = - mock<NotificationIconContainerStatusBarViewBinder>(), - broadcastDispatcher = broadcastDispatcher, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index 7a2b7c24252b..51bb94fd2ab9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -25,15 +25,18 @@ import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInterac import com.android.systemui.keyguard.ui.viewmodel.alternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.alternateBouncerToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.aodToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.dozingToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dreamingToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.goneToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.goneToDozingTransitionViewModel @@ -81,6 +84,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, aodToPrimaryBouncerTransitionViewModel = aodToPrimaryBouncerTransitionViewModel, + dozingToDreamingTransitionViewModel = dozingToDreamingTransitionViewModel, dozingToGlanceableHubTransitionViewModel = dozingToGlanceableHubTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel, @@ -110,5 +114,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { headsUpNotificationInteractor = { headsUpNotificationInteractor }, largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }, unfoldTransitionInteractor = unfoldTransitionInteractor, + glanceableHubToAodTransitionViewModel = glanceableHubToAodTransitionViewModel, + aodToGlanceableHubTransitionViewModel = aodToGlanceableHubTransitionViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt index d787e2c190c8..91404e0688ed 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt @@ -30,6 +30,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.settings.userTracker +import com.android.systemui.shade.data.repository.shadeDialogContextInteractor import com.android.systemui.shade.domain.interactor.panelExpansionInteractor import com.android.systemui.shade.domain.interactor.shadeAnimationInteractor import com.android.systemui.shade.shadeController @@ -52,6 +53,7 @@ val Kosmos.statusBarNotificationActivityStarter by Kosmos.Fixture { StatusBarNotificationActivityStarter( applicationContext, + shadeDialogContextInteractor, fakeExecutorHandler, fakeExecutor, notificationVisibilityProvider, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/DisableChipsModernization.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/DisableChipsModernization.kt new file mode 100644 index 000000000000..69eecc0f082b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/DisableChipsModernization.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2025 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.phone.ongoingcall + +import android.platform.test.annotations.DisableFlags + +/** Disables all the flags necessary for [StatusBarChipsModernization.isEnabled] to return false. */ +@DisableFlags(StatusBarChipsModernization.FLAG_NAME) +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class DisableChipsModernization diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/EnableChipsModernization.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/EnableChipsModernization.kt new file mode 100644 index 000000000000..caa4373a2037 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/EnableChipsModernization.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 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.phone.ongoingcall + +import android.platform.test.annotations.EnableFlags +import com.android.systemui.statusbar.core.StatusBarRootModernization + +/** Enables all the flags necessary for [StatusBarChipsModernization.isEnabled] to return true. */ +@EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class EnableChipsModernization diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt index d09d010cba2e..8ff7c7d01fb3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt @@ -76,6 +76,7 @@ object OngoingCallTestHelper { promotedContent: PromotedNotificationContentModel? = null, contentIntent: PendingIntent? = null, uid: Int = DEFAULT_UID, + appName: String = "Fake name", ) { if (StatusBarChipsModernization.isEnabled) { activeNotificationListRepository.addNotif( @@ -87,6 +88,7 @@ object OngoingCallTestHelper { contentIntent = contentIntent, promotedContent = promotedContent, uid = uid, + appName = appName, ) ) } else { @@ -96,6 +98,7 @@ object OngoingCallTestHelper { notificationIcon = statusBarChipIconView, intent = contentIntent, notificationKey = key, + appName = appName, promotedContent = promotedContent, ) ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 9b6f205fba72..8fa82cad5c32 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -58,6 +58,8 @@ class FakeMobileIconsInteractor( override val defaultDataSubId: MutableStateFlow<Int?> = MutableStateFlow(DEFAULT_DATA_SUB_ID) + override val activeMobileDataSubscriptionId: MutableStateFlow<Int?> = MutableStateFlow(null) + private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false) override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKosmos.kt new file mode 100644 index 000000000000..880ba5eee5d2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 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.pipeline.mobile.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.stackedMobileIconViewModel: StackedMobileIconViewModel by + Kosmos.Fixture { StackedMobileIconViewModel(mobileIconsViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapterKosmos.kt index 4e3c3caa2428..d4d589663477 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapterKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapterKosmos.kt @@ -21,9 +21,9 @@ import com.android.systemui.statusbar.phone.ui.statusBarIconController import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.wifiViewModel val Kosmos.wifiUiAdapter by -Kosmos.Fixture { - WifiUiAdapter( + Kosmos.Fixture { + WifiUiAdapter( statusBarIconController, wifiViewModel, - ) -} + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelKosmos.kt index 15057cd5bd0c..3f876ef0536b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelKosmos.kt @@ -24,14 +24,14 @@ import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.airplaneMod import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.wifiInteractor import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import org.mockito.kotlin.mock val Kosmos.wifiViewModel by - Kosmos.Fixture { + Kosmos.Fixture { WifiViewModel( airplaneModeViewModel, - { MutableStateFlow(false) }, + { flowOf(false) }, mock<ConnectivityConstants>(), applicationContext, logcatTableLogBuffer(this, "WifiViewModelTest"), @@ -39,4 +39,4 @@ val Kosmos.wifiViewModel by applicationCoroutineScope, mock<WifiConstants>(), ) - } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt index fd955089cdc7..933c351679a4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt @@ -17,7 +17,10 @@ package com.android.systemui.user.domain.interactor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.user.data.repository.userRepository val Kosmos.userLockedInteractor by - Kosmos.Fixture { UserLockedInteractor(userRepository = userRepository) } + Kosmos.Fixture { + UserLockedInteractor(backgroundDispatcher = testDispatcher, userRepository = userRepository) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt index 06af32e69b75..b23ccbfbd93f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt @@ -14,12 +14,15 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.util.time import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.currentTime var Kosmos.systemClock by @@ -27,6 +30,7 @@ var Kosmos.systemClock by mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } whenever(uptimeMillis()).thenAnswer { testScope.currentTime } + whenever(currentTimeMillis()).thenAnswer { testScope.currentTime } } } diff --git a/packages/Vcn/TEST_MAPPING b/packages/Vcn/TEST_MAPPING index 9722a838ab8e..9ca5304eb8d9 100644 --- a/packages/Vcn/TEST_MAPPING +++ b/packages/Vcn/TEST_MAPPING @@ -14,5 +14,13 @@ { "name": "CtsVcnTestCases" } + ], + "tethering-mainline-presubmit": [ + { + "name": "FrameworksVcnTests" + }, + { + "name": "CtsVcnTestCases" + } ] }
\ No newline at end of file diff --git a/packages/Vcn/framework-b/Android.bp b/packages/Vcn/framework-b/Android.bp index edb22c0e7aa0..c53123359872 100644 --- a/packages/Vcn/framework-b/Android.bp +++ b/packages/Vcn/framework-b/Android.bp @@ -77,8 +77,7 @@ framework_connectivity_b_defaults_soong_config { ], soong_config_variables: { is_vcn_in_mainline: { - //TODO: b/380155299 Make it Baklava when aidl tool can understand it - min_sdk_version: "current", + min_sdk_version: "36", static_libs: ["android.net.vcn.flags-aconfig-java"], apex_available: ["com.android.tethering"], diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index bb0eacb5afa7..5e1fe8a60973 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -11,6 +11,16 @@ flag { } flag { + name: "allow_secure_screenshots" + namespace: "accessibility" + description: "Allow certain AccessibilityServices to take screenshots of FLAG_SECURE screens" + bug: "373705911" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "always_allow_observing_touch_events" namespace: "accessibility" description: "Always allows InputFilter observing SOURCE_TOUCHSCREEN events, even if touch exploration is enabled." diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 9ceca5d1dbfe..4b042489f3eb 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -86,11 +86,13 @@ import android.view.MagnificationSpec; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; +import android.view.WindowManager; import android.view.accessibility.AccessibilityCache; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.view.accessibility.IWindowSurfaceInfoCallback; import android.view.inputmethod.EditorInfo; import android.window.ScreenCapture; import android.window.ScreenCapture.ScreenshotHardwareBuffer; @@ -343,6 +345,8 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback); + int performScreenCapture(ScreenCapture.LayerCaptureArgs captureArgs, + ScreenCapture.ScreenCaptureListener captureListener); } public AbstractAccessibilityServiceConnection(Context context, ComponentName componentName, @@ -1456,7 +1460,45 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_WINDOW, interactionId); return; } - connection.getRemote().takeScreenshotOfWindow(interactionId, listener, callback); + if (Flags.allowSecureScreenshots()) { + IWindowSurfaceInfoCallback infoCallback = new IWindowSurfaceInfoCallback.Stub() { + @Override + public void provideWindowSurfaceInfo(int windowFlags, int processUid, + SurfaceControl surfaceControl) { + final boolean canCaptureSecureLayers = canCaptureSecureLayers(); + if (!canCaptureSecureLayers + && (windowFlags & WindowManager.LayoutParams.FLAG_SECURE) != 0) { + try { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_SECURE_WINDOW, + interactionId); + } catch (RemoteException e) { + // ignore - the other side will time out + } + return; + } + + final ScreenCapture.LayerCaptureArgs captureArgs = + new ScreenCapture.LayerCaptureArgs.Builder(surfaceControl) + .setChildrenOnly(false) + .setUid(processUid) + .setCaptureSecureLayers(canCaptureSecureLayers) + .build(); + if (mSystemSupport.performScreenCapture(captureArgs, listener) != 0) { + try { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, + interactionId); + } catch (RemoteException e) { + // ignore - the other side will time out + } + } + } + }; + connection.getRemote().getWindowSurfaceInfo(infoCallback); + } else { + connection.getRemote().takeScreenshotOfWindow(interactionId, listener, callback); + } } finally { Binder.restoreCallingIdentity(identity); } @@ -1523,7 +1565,14 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } } ); - mWindowManagerService.captureDisplay(displayId, null, screenCaptureListener); + if (Flags.allowSecureScreenshots()) { + mWindowManagerService.captureDisplay(displayId, + new ScreenCapture.CaptureArgs.Builder<>() + .setCaptureSecureLayers(canCaptureSecureLayers()).build(), + screenCaptureListener); + } else { + mWindowManagerService.captureDisplay(displayId, null, screenCaptureListener); + } } catch (Exception e) { sendScreenshotFailure(AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY, callback); @@ -1564,6 +1613,13 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ }, null).recycleOnUse()); } + private boolean canCaptureSecureLayers() { + return Flags.allowSecureScreenshots() + && mAccessibilityServiceInfo.isAccessibilityTool() + && mAccessibilityServiceInfo.getResolveInfo().serviceInfo + .applicationInfo.isSystemApp(); + } + @Override @PermissionManuallyEnforced public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java index e422fef6c22c..9b5f22afb81d 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -134,7 +134,7 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo */ static final int FLAG_SERVICE_HANDLES_DOUBLE_TAP = 0x00000080; -/** + /** * Flag for enabling multi-finger gestures. * * @see #setUserAndEnabledFeatures(int, int) @@ -190,8 +190,6 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo private final AccessibilityManagerService mAms; - private final InputManager mInputManager; - private final SparseArray<EventStreamTransformation> mEventHandler; private final SparseArray<TouchExplorer> mTouchExplorer = new SparseArray<>(0); @@ -294,7 +292,6 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo mContext = context; mAms = service; mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mInputManager = context.getSystemService(InputManager.class); mEventHandler = eventHandler; } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 67fdca446ba4..9eb8442be783 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -173,6 +173,7 @@ import android.view.accessibility.IAccessibilityManagerClient; import android.view.accessibility.IMagnificationConnection; import android.view.accessibility.IUserInitializationCompleteCallback; import android.view.inputmethod.EditorInfo; +import android.window.ScreenCapture; import com.android.internal.R; import com.android.internal.accessibility.AccessibilityShortcutController; @@ -6725,6 +6726,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub callback)); } + @Override + public int performScreenCapture(ScreenCapture.LayerCaptureArgs captureArgs, + ScreenCapture.ScreenCaptureListener captureListener) { + return ScreenCapture.captureLayers(captureArgs, captureListener); + } + @VisibleForTesting int getShortcutTypeForGenericShortcutCalls(int userId) { int navigationMode = Settings.Secure.getIntForUser( diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 4fa0d506f09e..aa82df493f84 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -101,8 +101,15 @@ public class AutoclickController extends BaseEventStreamTransformation { } @Override - public void toggleAutoclickPause() { - // TODO(b/388872274): allows users to pause the autoclick. + public void toggleAutoclickPause(boolean paused) { + if (paused) { + if (mClickScheduler != null) { + mClickScheduler.cancel(); + } + if (mAutoclickIndicatorScheduler != null) { + mAutoclickIndicatorScheduler.cancel(); + } + } } }; @@ -133,7 +140,9 @@ public class AutoclickController extends BaseEventStreamTransformation { mAutoclickIndicatorScheduler); } - handleMouseMotion(event, policyFlags); + if (!isPaused()) { + handleMouseMotion(event, policyFlags); + } } else if (mClickScheduler != null) { mClickScheduler.cancel(); } @@ -216,6 +225,11 @@ public class AutoclickController extends BaseEventStreamTransformation { } } + private boolean isPaused() { + // TODO (b/397460424): Unpause when hovering over panel. + return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isPaused(); + } + /** * Observes autoclick setting values, and updates ClickScheduler delay and indicator size * whenever the setting value changes. diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickIndicatorView.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickIndicatorView.java index 01f359fc5753..6beb47acb8b1 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickIndicatorView.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickIndicatorView.java @@ -123,6 +123,7 @@ public class AutoclickIndicatorView extends View { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Get the screen dimensions. + // TODO(b/397944891): Handle device rotation case. DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); int screenWidth = displayMetrics.widthPixels; int screenHeight = displayMetrics.heightPixels; diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java index c74e4f5cd8d8..342675a65360 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java @@ -25,6 +25,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.view.Gravity; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; @@ -46,6 +47,21 @@ public class AutoclickTypePanel { public static final int AUTOCLICK_TYPE_DRAG = 3; public static final int AUTOCLICK_TYPE_SCROLL = 4; + public static final int CORNER_BOTTOM_RIGHT = 0; + public static final int CORNER_BOTTOM_LEFT = 1; + public static final int CORNER_TOP_LEFT = 2; + public static final int CORNER_TOP_RIGHT = 3; + + // Distance between panel and screen edge. + // TODO(b/396402941): Finalize edge margin. + private static final int PANEL_EDGE_MARGIN = 15; + + // Touch point when drag starts, it can be anywhere inside the panel. + private float mTouchStartX, mTouchStartY; + // Initial panel position in screen coordinates. + private int mPanelStartX, mPanelStartY; + private boolean mIsDragging = false; + // Types of click the AutoclickTypePanel supports. @IntDef({ AUTOCLICK_TYPE_LEFT_CLICK, @@ -56,14 +72,38 @@ public class AutoclickTypePanel { }) public @interface AutoclickType {} + @IntDef({ + CORNER_BOTTOM_RIGHT, + CORNER_BOTTOM_LEFT, + CORNER_TOP_LEFT, + CORNER_TOP_RIGHT + }) + public @interface Corner {} + + private static final @Corner int[] CORNER_ROTATION_ORDER = { + CORNER_BOTTOM_RIGHT, + CORNER_BOTTOM_LEFT, + CORNER_TOP_LEFT, + CORNER_TOP_RIGHT + }; + // An interface exposed to {@link AutoclickController) to handle different actions on the panel, // including changing autoclick type, pausing/resuming autoclick. public interface ClickPanelControllerInterface { - // Allows users to change a different autoclick type. + /** + * Allows users to change a different autoclick type. + * + * @param clickType The new autoclick type to use. Should be one of the values defined in + * {@link AutoclickType}. + */ void handleAutoclickTypeChange(@AutoclickType int clickType); - // Allows users to pause/resume the autoclick. - void toggleAutoclickPause(); + /** + * Allows users to pause or resume autoclick. + * + * @param paused {@code true} to pause autoclick, {@code false} to resume. + */ + void toggleAutoclickPause(boolean paused); } private final Context mContext; @@ -72,6 +112,8 @@ public class AutoclickTypePanel { private final WindowManager mWindowManager; + private WindowManager.LayoutParams mParams; + private final ClickPanelControllerInterface mClickPanelController; // Whether the panel is expanded or not. @@ -79,6 +121,10 @@ public class AutoclickTypePanel { // Whether autoclick is paused. private boolean mPaused = false; + // Tracks the current corner position of the panel using an index into CORNER_ROTATION_ORDER + // array. This allows the panel to cycle through screen corners in a defined sequence when + // repositioned. + private int mCurrentCornerIndex = 0; private final LinearLayout mLeftClickButton; private final LinearLayout mRightClickButton; @@ -86,6 +132,7 @@ public class AutoclickTypePanel { private final LinearLayout mDragButton; private final LinearLayout mScrollButton; private final LinearLayout mPauseButton; + private final LinearLayout mPositionButton; private LinearLayout mSelectedButton; @@ -99,6 +146,7 @@ public class AutoclickTypePanel { mContext = context; mWindowManager = windowManager; mClickPanelController = clickPanelController; + mParams = getDefaultLayoutParams(); mPauseButtonDrawable = mContext.getDrawable( R.drawable.accessibility_autoclick_pause); @@ -117,8 +165,94 @@ public class AutoclickTypePanel { mScrollButton = mContentView.findViewById(R.id.accessibility_autoclick_scroll_layout); mDragButton = mContentView.findViewById(R.id.accessibility_autoclick_drag_layout); mPauseButton = mContentView.findViewById(R.id.accessibility_autoclick_pause_layout); + mPositionButton = mContentView.findViewById(R.id.accessibility_autoclick_position_layout); initializeButtonState(); + + // Set up touch event handling for the panel to allow the user to drag and reposition the + // panel by touching and moving it. + mContentView.setOnTouchListener(this::onPanelTouch); + } + + /** + * Handles touch events on the panel, enabling the user to drag and reposition it. + * This function supports the draggable panel feature, allowing users to move the panel + * to different screen locations for better usability and customization. + */ + private boolean onPanelTouch(View v, MotionEvent event) { + // TODO(b/397681794): Make sure this works on multiple screens. + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // Store initial touch positions. + mTouchStartX = event.getRawX(); + mTouchStartY = event.getRawY(); + + // Store initial panel position relative to screen's top-left corner. + // getLocationOnScreen provides coordinates relative to the top-left corner of the + // screen's display. We are using this coordinate system to consistently track the + // panel's position during drag operations. + int[] location = new int[2]; + v.getLocationOnScreen(location); + mPanelStartX = location[0]; + mPanelStartY = location[1]; + return true; + case MotionEvent.ACTION_MOVE: + mIsDragging = true; + + // Set panel gravity to TOP|LEFT to match getLocationOnScreen's coordinate system + mParams.gravity = Gravity.LEFT | Gravity.TOP; + + if (mIsDragging) { + // Calculate touch distance moved from start position. + float deltaX = event.getRawX() - mTouchStartX; + float deltaY = event.getRawY() - mTouchStartY; + + // Update panel position, based on Top-Left absolute positioning. + mParams.x = mPanelStartX + (int) deltaX; + mParams.y = mPanelStartY + (int) deltaY; + mWindowManager.updateViewLayout(mContentView, mParams); + } + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mIsDragging) { + // When drag ends, snap panel to nearest edge. + snapToNearestEdge(mParams); + } + mIsDragging = false; + return true; + } + return false; + } + + private void snapToNearestEdge(WindowManager.LayoutParams params) { + // Get screen width to determine which side to snap to. + // TODO(b/397944891): Handle device rotation case. + int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels; + int yPosition = params.y; + + // Determine which half of the screen the panel is on. + boolean isOnLeftHalf = params.x < screenWidth / 2; + + if (isOnLeftHalf) { + // Snap to left edge. Set params.gravity to make sure x, y offsets from correct anchor. + params.gravity = Gravity.START | Gravity.TOP; + // Set the current corner to be bottom-left to ensure that the subsequent reposition + // action rotates the panel clockwise from bottom-left towards top-left. + mCurrentCornerIndex = 1; + } else { + // Snap to right edge. Set params.gravity to make sure x, y offsets from correct anchor. + params.gravity = Gravity.END | Gravity.TOP; + // Set the current corner to be top-right to ensure that the subsequent reposition + // action rotates the panel clockwise from top-right towards bottom-right. + mCurrentCornerIndex = 3; + } + + // Apply final position: set params.x to be edge margin, params.y to maintain vertical + // position. + params.x = PANEL_EDGE_MARGIN; + params.y = yPosition; + mWindowManager.updateViewLayout(mContentView, params); } private void initializeButtonState() { @@ -128,10 +262,8 @@ public class AutoclickTypePanel { v -> togglePanelExpansion(AUTOCLICK_TYPE_DOUBLE_CLICK)); mScrollButton.setOnClickListener(v -> togglePanelExpansion(AUTOCLICK_TYPE_SCROLL)); mDragButton.setOnClickListener(v -> togglePanelExpansion(AUTOCLICK_TYPE_DRAG)); - + mPositionButton.setOnClickListener(v -> moveToNextCorner()); mPauseButton.setOnClickListener(v -> togglePause()); - // TODO(b/388847771): registers listener for position button and allows users to move the - // panel to a different position. // Initializes panel as collapsed state and only displays the left click button. hideAllClickTypeButtons(); @@ -176,7 +308,7 @@ public class AutoclickTypePanel { } public void show() { - mWindowManager.addView(mContentView, getLayoutParams()); + mWindowManager.addView(mContentView, mParams); } public void hide() { @@ -187,6 +319,10 @@ public class AutoclickTypePanel { mWindowManager.removeView(mContentView); } + public boolean isPaused() { + return mPaused; + } + /** Toggles the panel expanded or collapsed state. */ private void togglePanelExpansion(@AutoclickType int clickType) { final LinearLayout button = getButtonFromClickType(clickType); @@ -210,6 +346,7 @@ public class AutoclickTypePanel { private void togglePause() { mPaused = !mPaused; + mClickPanelController.toggleAutoclickPause(mPaused); ImageButton imageButton = (ImageButton) mPauseButton.getChildAt(/* index= */ 0); if (mPaused) { @@ -248,6 +385,45 @@ public class AutoclickTypePanel { }; } + /** Moves the panel to the next corner in clockwise direction. */ + private void moveToNextCorner() { + @Corner int nextCornerIndex = (mCurrentCornerIndex + 1) % CORNER_ROTATION_ORDER.length; + mCurrentCornerIndex = nextCornerIndex; + + setPanelPositionForCorner(mParams, mCurrentCornerIndex); + mWindowManager.updateViewLayout(mContentView, mParams); + } + + private void setPanelPositionForCorner(WindowManager.LayoutParams params, @Corner int corner) { + // TODO(b/396402941): Replace hardcoded pixel values with proper dimension calculations, + // Current values are experimental and may not work correctly across different device + // resolutions and configurations. + switch (corner) { + case CORNER_BOTTOM_RIGHT: + params.gravity = Gravity.END | Gravity.BOTTOM; + params.x = PANEL_EDGE_MARGIN; + params.y = 90; + break; + case CORNER_BOTTOM_LEFT: + params.gravity = Gravity.START | Gravity.BOTTOM; + params.x = PANEL_EDGE_MARGIN; + params.y = 90; + break; + case CORNER_TOP_LEFT: + params.gravity = Gravity.START | Gravity.TOP; + params.x = PANEL_EDGE_MARGIN; + params.y = 30; + break; + case CORNER_TOP_RIGHT: + params.gravity = Gravity.END | Gravity.TOP; + params.x = PANEL_EDGE_MARGIN; + params.y = 30; + break; + default: + throw new IllegalArgumentException("Invalid corner: " + corner); + } + } + @VisibleForTesting boolean getExpansionStateForTesting() { return mExpanded; @@ -259,12 +435,28 @@ public class AutoclickTypePanel { return mContentView; } + @VisibleForTesting + @Corner + int getCurrentCornerIndexForTesting() { + return mCurrentCornerIndex; + } + + @VisibleForTesting + WindowManager.LayoutParams getLayoutParamsForTesting() { + return mParams; + } + + @VisibleForTesting + boolean getIsDraggingForTesting() { + return mIsDragging; + } + /** * Retrieves the layout params for AutoclickIndicatorView, used when it's added to the Window * Manager. */ @NonNull - private WindowManager.LayoutParams getLayoutParams() { + private WindowManager.LayoutParams getDefaultLayoutParams() { final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; @@ -277,11 +469,7 @@ public class AutoclickTypePanel { mContext.getString(R.string.accessibility_autoclick_type_settings_panel_title); layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; - // TODO(b/388847771): Compute position based on user interaction. - layoutParams.x = 15; - layoutParams.y = 90; - layoutParams.gravity = Gravity.END | Gravity.BOTTOM; - + setPanelPositionForCorner(layoutParams, CORNER_BOTTOM_RIGHT); return layoutParams; } } diff --git a/services/appwidget/Android.bp b/services/appwidget/Android.bp index 8119073fdf7f..9548afe398b1 100644 --- a/services/appwidget/Android.bp +++ b/services/appwidget/Android.bp @@ -17,6 +17,12 @@ filegroup { java_library_static { name: "services.appwidget", defaults: ["platform_service_defaults"], - srcs: [":services.appwidget-sources"], - libs: ["services.core"], + srcs: [ + ":services.appwidget-sources", + ":statslog-framework-java-gen", + ], + libs: [ + "androidx.annotation_annotation", + "services.core", + ], } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index e0f2939a2083..4441db78e4b5 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -50,6 +50,7 @@ import android.app.IApplicationThread; import android.app.IServiceConnection; import android.app.KeyguardManager; import android.app.PendingIntent; +import android.app.StatsManager; import android.app.admin.DevicePolicyManagerInternal; import android.app.admin.DevicePolicyManagerInternal.OnCrossProfileWidgetProvidersChangeListener; import android.app.usage.Flags; @@ -124,6 +125,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.SparseLongArray; +import android.util.StatsEvent; import android.util.TypedValue; import android.util.Xml; import android.util.proto.ProtoInputStream; @@ -145,6 +147,7 @@ import com.android.internal.os.BackgroundThread; import com.android.internal.os.SomeArgs; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.widget.IRemoteViewsFactory; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -433,6 +436,44 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mAppOpsManagerInternal = LocalServices.getService(AppOpsManagerInternal.class); mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class); + registerPullCallbacks(); + } + + /** + * Register callbacks for pull atoms. + */ + private void registerPullCallbacks() { + final StatsManager manager = mContext.getSystemService(StatsManager.class); + manager.setPullAtomCallback(FrameworkStatsLog.WIDGET_MEMORY_STATS, + new StatsManager.PullAtomMetadata.Builder().build(), + new HandlerExecutor(mCallbackHandler), this::onPullAtom); + } + + /** + * Callback from StatsManager to log events indicated by the atomTag. This function will add + * the relevant events to the data list. + * + * @return PULL_SUCCESS if the pull was successful and events should be used, else PULL_SKIP. + */ + private int onPullAtom(int atomTag, @NonNull List<StatsEvent> data) { + if (atomTag == FrameworkStatsLog.WIDGET_MEMORY_STATS) { + synchronized (mLock) { + for (Widget widget : mWidgets) { + if (widget.views != null) { + final int uid = widget.provider.id.uid; + final int appWidgetId = widget.appWidgetId; + final long bitmapMemoryUsage = + widget.views.estimateTotalBitmapMemoryUsage(); + StatsEvent event = FrameworkStatsLog.buildStatsEvent( + FrameworkStatsLog.WIDGET_MEMORY_STATS, uid, appWidgetId, + bitmapMemoryUsage); + data.add(event); + } + } + } + return StatsManager.PULL_SUCCESS; + } + return StatsManager.PULL_SKIP; } /** @@ -5191,7 +5232,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } return singleCategoryKeyedEntries; - } catch (IOException e) { + } catch (Exception e) { Slog.e(TAG, "Failed to load generated previews for " + provider, e); return new SparseArray<>(); } @@ -5220,7 +5261,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku try { provider.info.generatedPreviewCategories = readGeneratedPreviewCategoriesFromProto( input); - } catch (IOException e) { + } catch (Exception e) { Slog.e(TAG, "Failed to read generated previews from file for " + provider, e); previewsFile.delete(); provider.info.generatedPreviewCategories = 0; @@ -5273,7 +5314,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku scheduleNotifyGroupHostsForProvidersChangedLocked(provider.getUserId()); } } - } catch (IOException e) { + } catch (Exception e) { if (file != null && stream != null) { file.failWrite(stream); } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 05301fdd8385..4f56483f487e 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -31,6 +31,8 @@ import static android.os.UserHandle.getCallingUserId; import static com.android.internal.util.CollectionUtils.any; import static com.android.internal.util.Preconditions.checkState; +import static com.android.server.companion.association.DisassociationProcessor.REASON_API; +import static com.android.server.companion.association.DisassociationProcessor.REASON_PKG_DATA_CLEARED; import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature; import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; @@ -250,7 +252,7 @@ public class CompanionDeviceManagerService extends SystemService { + packageName + "]. Cleaning up CDM data..."); for (AssociationInfo association : associationsForPackage) { - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(association.getId(), REASON_PKG_DATA_CLEARED); } mCompanionAppBinder.onPackageChanged(userId); @@ -426,7 +428,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public void disassociate(int associationId) { - mDisassociationProcessor.disassociate(associationId); + mDisassociationProcessor.disassociate(associationId, REASON_API); } @Override diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index e7d1460aa66a..c5ac7c31b5c3 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -18,6 +18,8 @@ package com.android.server.companion; import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC; +import static com.android.server.companion.association.DisassociationProcessor.REASON_SHELL; + import android.companion.AssociationInfo; import android.companion.ContextSyncMessage; import android.companion.Flags; @@ -122,7 +124,7 @@ class CompanionDeviceShellCommand extends ShellCommand { if (association == null) { out.println("Association doesn't exist."); } else { - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(association.getId(), REASON_SHELL); } } break; @@ -132,7 +134,7 @@ class CompanionDeviceShellCommand extends ShellCommand { final List<AssociationInfo> userAssociations = mAssociationStore.getAssociationsByUser(userId); for (AssociationInfo association : userAssociations) { - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(association.getId(), REASON_SHELL); } } break; diff --git a/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java index f2d019bde703..ce7dcd0fa1d4 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java @@ -58,6 +58,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; @@ -164,6 +165,7 @@ public final class AssociationDiskStore { private static final String FILE_NAME_LEGACY = "companion_device_manager_associations.xml"; private static final String FILE_NAME = "companion_device_manager.xml"; + private static final String FILE_NAME_LAST_REMOVED_ASSOCIATION = "last_removed_association.txt"; private static final String XML_TAG_STATE = "state"; private static final String XML_TAG_ASSOCIATIONS = "associations"; @@ -268,6 +270,46 @@ public final class AssociationDiskStore { } } + /** + * Read the last removed association from disk. + */ + public String readLastRemovedAssociation(@UserIdInt int userId) { + final AtomicFile file = createStorageFileForUser( + userId, FILE_NAME_LAST_REMOVED_ASSOCIATION); + StringBuilder sb = new StringBuilder(); + int c; + try (FileInputStream fis = file.openRead()) { + while ((c = fis.read()) != -1) { + sb.append((char) c); + } + fis.close(); + return sb.toString(); + } catch (FileNotFoundException e) { + Slog.e(TAG, "File " + file + " for user=" + userId + " doesn't exist."); + return null; + } catch (IOException e) { + Slog.e(TAG, "Can't read file " + file + " for user=" + userId); + return null; + } + } + + /** + * Write the last removed association to disk. + */ + public void writeLastRemovedAssociation(AssociationInfo association, String reason) { + Slog.i(TAG, "Writing last removed association=" + association.getId() + " to disk..."); + + final AtomicFile file = createStorageFileForUser( + association.getUserId(), FILE_NAME_LAST_REMOVED_ASSOCIATION); + writeToFileSafely(file, out -> { + out.write(String.valueOf(System.currentTimeMillis()).getBytes()); + out.write(' '); + out.write(reason.getBytes()); + out.write(' '); + out.write(association.toString().getBytes()); + }); + } + @NonNull private static Associations readAssociationsFromFile(@UserIdInt int userId, @NonNull AtomicFile file, @NonNull String rootTag) { diff --git a/services/companion/java/com/android/server/companion/association/AssociationStore.java b/services/companion/java/com/android/server/companion/association/AssociationStore.java index 757abd927ac8..f70c434e6b46 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java @@ -276,7 +276,7 @@ public class AssociationStore { /** * Remove an association. */ - public void removeAssociation(int id) { + public void removeAssociation(int id, String reason) { Slog.i(TAG, "Removing association id=[" + id + "]..."); final AssociationInfo association; @@ -291,6 +291,8 @@ public class AssociationStore { writeCacheToDisk(association.getUserId()); + mDiskStore.writeLastRemovedAssociation(association, reason); + Slog.i(TAG, "Done removing association."); } @@ -525,6 +527,14 @@ public class AssociationStore { out.append(" ").append(a.toString()).append('\n'); } } + + out.append("Last Removed Association:\n"); + for (UserInfo user : mUserManager.getAliveUsers()) { + String lastRemovedAssociation = mDiskStore.readLastRemovedAssociation(user.id); + if (lastRemovedAssociation != null) { + out.append(" ").append(lastRemovedAssociation).append('\n'); + } + } } private void broadcastChange(@ChangeType int changeType, AssociationInfo association) { diff --git a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java index 150e8da5f614..248056f32a4f 100644 --- a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java +++ b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java @@ -47,6 +47,13 @@ import com.android.server.companion.transport.CompanionTransportManager; @SuppressLint("LongLogTag") public class DisassociationProcessor { + public static final String REASON_REVOKED = "revoked"; + public static final String REASON_SELF_IDLE = "self-idle"; + public static final String REASON_SHELL = "shell"; + public static final String REASON_LEGACY = "legacy"; + public static final String REASON_API = "api"; + public static final String REASON_PKG_DATA_CLEARED = "pkg-data-cleared"; + private static final String TAG = "CDM_DisassociationProcessor"; private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW = @@ -94,7 +101,7 @@ public class DisassociationProcessor { * Disassociate an association by id. */ // TODO: also revoke notification access - public void disassociate(int id) { + public void disassociate(int id, String reason) { Slog.i(TAG, "Disassociating id=[" + id + "]..."); final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id); @@ -126,7 +133,7 @@ public class DisassociationProcessor { // Association cleanup. mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id); - mAssociationStore.removeAssociation(association.getId()); + mAssociationStore.removeAssociation(association.getId(), reason); // If role is not in use by other associations, revoke the role. // Do not need to remove the system role since it was pre-granted by the system. @@ -151,7 +158,7 @@ public class DisassociationProcessor { } /** - * @deprecated Use {@link #disassociate(int)} instead. + * @deprecated Use {@link #disassociate(int, String)} instead. */ @Deprecated public void disassociate(int userId, String packageName, String macAddress) { @@ -165,7 +172,7 @@ public class DisassociationProcessor { mAssociationStore.getAssociationWithCallerChecks(association.getId()); - disassociate(association.getId()); + disassociate(association.getId(), REASON_LEGACY); } @SuppressLint("MissingPermission") @@ -223,7 +230,7 @@ public class DisassociationProcessor { Slog.i(TAG, "Removing inactive self-managed association=[" + association.toShortString() + "]."); - disassociate(id); + disassociate(id, REASON_SELF_IDLE); } } @@ -234,7 +241,7 @@ public class DisassociationProcessor { * * Lastly remove the role holder for the revoked associations for the same packages. * - * @see #disassociate(int) + * @see #disassociate(int, String) */ private class OnPackageVisibilityChangeListener implements ActivityManager.OnUidImportanceListener { @@ -260,7 +267,7 @@ public class DisassociationProcessor { int userId = UserHandle.getUserId(uid); for (AssociationInfo association : mAssociationStore.getRevokedAssociations(userId, packageName)) { - disassociate(association.getId()); + disassociate(association.getId(), REASON_REVOKED); } if (mAssociationStore.getRevokedAssociations().isEmpty()) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 139bbae26289..93b4de856463 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -891,6 +891,11 @@ public class VirtualDeviceManagerService extends SystemService { } @Override + public VirtualDevice getVirtualDevice(int deviceId) { + return mImpl.getVirtualDevice(deviceId); + } + + @Override public long getDimDurationMillisForDeviceId(int deviceId) { VirtualDeviceImpl virtualDevice = getVirtualDeviceForId(deviceId); return virtualDevice == null ? -1 : virtualDevice.getDimDurationMillis(); diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java index d8e10f842665..7eb7072520de 100644 --- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java +++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java @@ -365,7 +365,7 @@ public class ContextualSearchManagerService extends SystemService { } } final ScreenshotHardwareBuffer shb = mWmInternal.takeContextualSearchScreenshot( - (Flags.contextualSearchWindowLayer() ? csUid : -1)); + (Flags.contextualSearchPreventSelfCapture() ? csUid : -1)); final Bitmap bm = shb != null ? shb.asBitmap() : null; // Now that everything is fetched, putting it in the launchIntent. if (bm != null) { @@ -549,7 +549,7 @@ public class ContextualSearchManagerService extends SystemService { Binder.withCleanCallingIdentity(() -> { final ScreenshotHardwareBuffer shb = mWmInternal.takeContextualSearchScreenshot( - (Flags.contextualSearchWindowLayer() ? callingUid : -1)); + (Flags.contextualSearchPreventSelfCapture() ? callingUid : -1)); final Bitmap bm = shb != null ? shb.asBitmap() : null; if (bm != null) { bundle.putParcelable(ContextualSearchManager.EXTRA_SCREENSHOT, diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index d2a5734f323f..b6fe0ad37078 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -49,7 +49,6 @@ import static com.android.internal.util.XmlUtils.writeStringAttribute; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.START_TAG; -import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -2443,6 +2442,7 @@ class StorageManagerService extends IStorageManager.Stub } catch (Installer.InstallerException e) { Slog.e(TAG, "Failed unmount mirror data", e); } + extendWatchdogTimeout("#unmount might be slow"); mVold.unmount(vol.getId()); mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo()); } catch (Exception e) { diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index cce29592d912..125824c3f37e 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -618,7 +618,7 @@ public final class ActiveServices { Slog.i(TAG, " Stopping fg for service " + r); } setServiceForegroundInnerLocked(r, 0, null, 0, 0, - 0); + 0, /* systemRequestedTransition= */ true); } } @@ -1839,7 +1839,7 @@ public final class ActiveServices { ServiceRecord r = findServiceLocked(className, token, userId); if (r != null) { setServiceForegroundInnerLocked(r, id, notification, flags, foregroundServiceType, - callingUid); + callingUid, /* systemRequestedTransition= */ false); } } finally { mAm.mInjector.restoreCallingIdentity(origId); @@ -2155,7 +2155,7 @@ public final class ActiveServices { @GuardedBy("mAm") private void setServiceForegroundInnerLocked(final ServiceRecord r, int id, Notification notification, int flags, int foregroundServiceType, - int callingUidIfStart) { + int callingUidIfStart, boolean systemRequestedTransition) { if (id != 0) { if (notification == null) { throw new IllegalArgumentException("null notification"); @@ -2800,6 +2800,7 @@ public final class ActiveServices { // earlier. r.foregroundServiceType = 0; r.mFgsNotificationWasDeferred = false; + r.systemRequestedFgToBg = systemRequestedTransition; signalForegroundServiceObserversLocked(r); resetFgsRestrictionLocked(r); mAm.updateForegroundServiceUsageStats(r.name, r.userId, false); @@ -9339,14 +9340,22 @@ public final class ActiveServices { if (sr.foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE && sr.foregroundId == notificationId) { - if (DEBUG_FOREGROUND_SERVICE) { - Slog.d(TAG, "Moving media service to foreground for package " - + packageName); + // check if service is explicitly requested by app to not be in foreground. + if (sr.systemRequestedFgToBg) { + Slog.d(TAG, + "System initiated service transition to foreground " + + "for package " + + packageName); + setServiceForegroundInnerLocked(sr, sr.foregroundId, + sr.foregroundNoti, /* flags */ 0, + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK, + /* callingUidStart */ 0, /* systemRequestedTransition */ true); + } else { + Slog.d(TAG, + "Ignoring system initiated foreground service transition for " + + "package" + + packageName); } - setServiceForegroundInnerLocked(sr, sr.foregroundId, - sr.foregroundNoti, /* flags */ 0, - ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK, - /* callingUidStart */ 0); } } } @@ -9379,13 +9388,14 @@ public final class ActiveServices { if (sr.foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK && sr.foregroundId == notificationId) { - if (DEBUG_FOREGROUND_SERVICE) { - Slog.d(TAG, "Forcing media foreground service to background for package " - + packageName); - } + Slog.d(TAG, + "System initiated transition of foreground service(type:media) to bg " + + "for package" + + packageName); setServiceForegroundInnerLocked(sr, /* id */ 0, /* notification */ null, /* flags */ 0, - /* foregroundServiceType */ 0, /* callingUidStart */ 0); + /* foregroundServiceType */ 0, /* callingUidStart */ 0, + /* systemRequestedTransition */ true); } } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 07a4d52f56ec..8b701f0e2069 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -198,6 +198,7 @@ import android.annotation.Nullable; import android.annotation.PermissionMethod; import android.annotation.PermissionName; import android.annotation.RequiresPermission; +import android.annotation.SpecialUsers.CanBeALL; import android.annotation.UserIdInt; import android.app.Activity; import android.app.ActivityClient; @@ -3921,8 +3922,8 @@ public class ActivityManagerService extends IActivityManager.Stub * The pkg name and app id have to be specified. */ @Override - public void killApplication(String pkg, int appId, int userId, String reason, - int exitInfoReason) { + public void killApplication(String pkg, int appId, @CanBeALL @UserIdInt int userId, + String reason, int exitInfoReason) { if (pkg == null) { return; } @@ -4307,7 +4308,7 @@ public class ActivityManagerService extends IActivityManager.Stub final boolean forceStopPackageLocked(String packageName, int appId, boolean callerWillRestart, boolean purgeCache, boolean doit, boolean evenPersistent, boolean uninstalling, boolean packageStateStopped, - int userId, String reasonString, int reason) { + @CanBeALL @UserIdInt int userId, String reasonString, int reason) { return forceStopPackageInternalLocked(packageName, appId, callerWillRestart, purgeCache, doit, evenPersistent, uninstalling, packageStateStopped, userId, reasonString, reason, ProcessList.INVALID_ADJ); @@ -4317,7 +4318,7 @@ public class ActivityManagerService extends IActivityManager.Stub private boolean forceStopPackageInternalLocked(String packageName, int appId, boolean callerWillRestart, boolean purgeCache, boolean doit, boolean evenPersistent, boolean uninstalling, boolean packageStateStopped, - int userId, String reasonString, int reason, int minOomAdj) { + @CanBeALL @UserIdInt int userId, String reasonString, int reason, int minOomAdj) { int i; if (userId == UserHandle.USER_ALL && packageName == null) { @@ -18093,7 +18094,7 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void killApplicationSync(String pkgName, int appId, int userId, + public void killApplicationSync(String pkgName, int appId, @CanBeALL @UserIdInt int userId, String reason, int exitInfoReason) { if (pkgName == null) { return; diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index b677297dfef2..4bfee1d8398f 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -1464,7 +1464,10 @@ public class CachedAppOptimizer { void onProcessFrozen(ProcessRecord frozenProc) { if (useCompaction()) { synchronized (mProcLock) { - compactApp(frozenProc, CompactProfile.FULL, CompactSource.APP, false); + // only full-compact if process is cached + if (frozenProc.mState.getSetAdj() >= mCompactThrottleMinOomAdj) { + compactApp(frozenProc, CompactProfile.FULL, CompactSource.APP, false); + } } } frozenProc.onProcessFrozen(); diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS index cc6fabc8fd67..4b6d6bc955cc 100644 --- a/services/core/java/com/android/server/am/OWNERS +++ b/services/core/java/com/android/server/am/OWNERS @@ -68,7 +68,7 @@ per-file CarUserSwitchingDialog.java = file:platform/packages/services/Car:/OWNE per-file ActivityManager* = file:/ACTIVITY_SECURITY_OWNERS # Aconfig Flags -per-file flags.aconfig = yamasani@google.com, bills@google.com, nalini@google.com +per-file flags.aconfig = yamasani@google.com, nalini@google.com # Londoners michaelwr@google.com #{LAST_RESORT_SUGGESTION} @@ -77,4 +77,4 @@ narayan@google.com #{LAST_RESORT_SUGGESTION} # Default yamasani@google.com hackbod@google.com #{LAST_RESORT_SUGGESTION} -omakoto@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file +omakoto@google.com #{LAST_RESORT_SUGGESTION} diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 61c5501a7b5a..13d367a95942 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -446,6 +446,8 @@ public class OomAdjuster { private static final int CACHING_UI_SERVICE_CLIENT_ADJ_THRESHOLD = Flags.raiseBoundUiServiceThreshold() ? SERVICE_ADJ : PERCEPTIBLE_APP_ADJ; + static final long PERCEPTIBLE_TASK_TIMEOUT_MILLIS = 5 * 60 * 1000; + @VisibleForTesting public static class Injector { boolean isChangeEnabled(@CachedCompatChangeId int cachedCompatChangeId, @@ -1847,7 +1849,7 @@ public class OomAdjuster { mHasVisibleActivities = false; } - void onOtherActivity() { + void onOtherActivity(long perceptibleTaskStoppedTimeMillis) { if (procState > PROCESS_STATE_CACHED_ACTIVITY) { procState = PROCESS_STATE_CACHED_ACTIVITY; mAdjType = "cch-act"; @@ -1856,6 +1858,28 @@ public class OomAdjuster { "Raise procstate to cached activity: " + app); } } + if (Flags.perceptibleTasks() && adj > PERCEPTIBLE_MEDIUM_APP_ADJ) { + if (perceptibleTaskStoppedTimeMillis >= 0) { + final long now = mInjector.getUptimeMillis(); + if (now - perceptibleTaskStoppedTimeMillis < PERCEPTIBLE_TASK_TIMEOUT_MILLIS) { + adj = PERCEPTIBLE_MEDIUM_APP_ADJ; + mAdjType = "perceptible-act"; + if (procState > PROCESS_STATE_IMPORTANT_BACKGROUND) { + procState = PROCESS_STATE_IMPORTANT_BACKGROUND; + } + + maybeSetProcessFollowUpUpdateLocked(app, + perceptibleTaskStoppedTimeMillis + PERCEPTIBLE_TASK_TIMEOUT_MILLIS, + now); + } else if (adj > PREVIOUS_APP_ADJ) { + adj = PREVIOUS_APP_ADJ; + mAdjType = "stale-perceptible-act"; + if (procState > PROCESS_STATE_LAST_ACTIVITY) { + procState = PROCESS_STATE_LAST_ACTIVITY; + } + } + } + } mHasVisibleActivities = false; } } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index b8babe69d5a7..a61368c4bc36 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -67,6 +67,8 @@ import static com.android.server.wm.WindowProcessController.STOPPED_STATE_FORCE_ import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; +import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManager.ProcessCapability; import android.app.ActivityThread; @@ -2961,8 +2963,8 @@ public final class ProcessList { } @GuardedBy({"mService", "mProcLock"}) - boolean killPackageProcessesLSP(String packageName, int appId, int userId, int minOomAdj, - int reasonCode, int subReason, String reason) { + boolean killPackageProcessesLSP(String packageName, int appId, @CanBeALL @UserIdInt int userId, + int minOomAdj, int reasonCode, int subReason, String reason) { return killPackageProcessesLSP(packageName, appId, userId, minOomAdj, false /* callerWillRestart */, true /* allowRestart */, true /* doit */, false /* evenPersistent */, false /* setRemoved */, false /* uninstalling */, @@ -2970,7 +2972,8 @@ public final class ProcessList { } @GuardedBy("mService") - void killAppZygotesLocked(String packageName, int appId, int userId, boolean force) { + void killAppZygotesLocked(String packageName, int appId, @CanBeALL @UserIdInt int userId, + boolean force) { // See if there are any app zygotes running for this packageName / UID combination, // and kill it if so. final ArrayList<AppZygote> zygotesToKill = new ArrayList<>(); @@ -3050,9 +3053,9 @@ public final class ProcessList { @GuardedBy({"mService", "mProcLock"}) boolean killPackageProcessesLSP(String packageName, int appId, - int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart, - boolean doit, boolean evenPersistent, boolean setRemoved, boolean uninstalling, - int reasonCode, int subReason, String reason) { + @CanBeALL @UserIdInt int userId, int minOomAdj, boolean callerWillRestart, + boolean allowRestart, boolean doit, boolean evenPersistent, boolean setRemoved, + boolean uninstalling, int reasonCode, int subReason, String reason) { final PackageManagerInternal pm = mService.getPackageManagerInternal(); final ArrayList<Pair<ProcessRecord, Boolean>> procs = new ArrayList<>(); @@ -5220,7 +5223,7 @@ public final class ProcessList { } @GuardedBy("mService") - void sendPackageBroadcastLocked(int cmd, String[] packages, int userId) { + void sendPackageBroadcastLocked(int cmd, String[] packages, @CanBeALL @UserIdInt int userId) { boolean foundProcess = false; for (int i = mLruProcesses.size() - 1; i >= 0; i--) { ProcessRecord r = mLruProcesses.get(i); diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java index b0f808b39053..25175e6bee5f 100644 --- a/services/core/java/com/android/server/am/ProcessStateRecord.java +++ b/services/core/java/com/android/server/am/ProcessStateRecord.java @@ -1120,7 +1120,8 @@ final class ProcessStateRecord { } else if ((flags & ACTIVITY_STATE_FLAG_IS_STOPPING) != 0) { callback.onStoppingActivity((flags & ACTIVITY_STATE_FLAG_IS_STOPPING_FINISHING) != 0); } else { - callback.onOtherActivity(); + final long ts = mApp.getWindowProcessController().getPerceptibleTaskStoppedTimeMillis(); + callback.onOtherActivity(ts); } mCachedAdj = callback.adj; diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index ca34a13c55b1..f443e4455734 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -158,6 +158,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN boolean fgWaiting; // is a timeout for going foreground already scheduled? boolean isNotAppComponentUsage; // is service binding not considered component/package usage? boolean isForeground; // is service currently in foreground mode? + boolean systemRequestedFgToBg; // system requested service to transition to background. boolean inSharedIsolatedProcess; // is the service in a shared isolated process int foregroundId; // Notification ID of last foreground req. Notification foregroundNoti; // Notification record of foreground state. diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index e0fbaf43ea43..18f3500b2d56 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -4059,7 +4059,7 @@ class UserController implements Handler.Callback { synchronized (mUserSwitchingDialogLock) { dismissUserSwitchingDialog(null); mUserSwitchingDialog = new UserSwitchingDialog(mService.mContext, fromUser, toUser, - switchingFromSystemUserMessage, switchingToSystemUserMessage); + mHandler, switchingFromSystemUserMessage, switchingToSystemUserMessage); mUserSwitchingDialog.show(onShown); } } diff --git a/services/core/java/com/android/server/am/UserSwitchingDialog.java b/services/core/java/com/android/server/am/UserSwitchingDialog.java index d1fcb9d1ca37..223e0b79ec0b 100644 --- a/services/core/java/com/android/server/am/UserSwitchingDialog.java +++ b/services/core/java/com/android/server/am/UserSwitchingDialog.java @@ -34,7 +34,6 @@ import android.graphics.drawable.Animatable2; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; -import android.os.Looper; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; @@ -72,7 +71,7 @@ class UserSwitchingDialog extends Dialog { // Time to wait for the onAnimationEnd() callbacks before moving on private static final int ANIMATION_TIMEOUT_MS = 1000; - private final Handler mHandler = new Handler(Looper.myLooper()); + private final Handler mHandler; protected final UserInfo mOldUser; protected final UserInfo mNewUser; @@ -81,13 +80,14 @@ class UserSwitchingDialog extends Dialog { protected final Context mContext; private final int mTraceCookie; - UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser, + UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser, Handler handler, String switchingFromSystemUserMessage, String switchingToSystemUserMessage) { super(context, R.style.Theme_Material_NoActionBar_Fullscreen); mContext = context; mOldUser = oldUser; mNewUser = newUser; + mHandler = handler; mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage; mSwitchingToSystemUserMessage = switchingToSystemUserMessage; mDisableAnimations = SystemProperties.getBoolean( diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 27c384a22fb6..c8fedf3d1765 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -293,6 +293,13 @@ flag { } flag { + name: "perceptible_tasks" + namespace: "system_performance" + description: "Boost the oom_score_adj of activities in perceptible tasks" + bug: "370890207" +} + +flag { name: "expedite_activity_launch_on_cold_start" namespace: "system_performance" description: "Notify ActivityTaskManager of cold starts early to fix app launch behavior." diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 813e661d6970..d800503c658e 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -9718,72 +9718,78 @@ public class AudioService extends IAudioService.Stub } } } - } - } - if (changed) { - // If associated to volume group, update group cache - updateVolumeGroupIndex(device, /* forceMuteState= */ false); - - oldIndex = (oldIndex + 5) / 10; - index = (index + 5) / 10; - // log base stream changes to the event log - if (sStreamVolumeAlias.get(mStreamType, /*valueIfKeyNotFound=*/-1) == mStreamType) { - if (caller == null) { - Log.w(TAG, "No caller for volume_changed event", new Throwable()); - } - EventLogTags.writeVolumeChanged(mStreamType, oldIndex, index, mIndexMax / 10, - caller); - } - // fire changed intents for all streams, but only when the device it changed on - // is the current device - if ((index != oldIndex) && isCurrentDevice) { - // for single volume devices, only send the volume change broadcast - // on the alias stream - final int streamAlias = sStreamVolumeAlias.get( - mStreamType, /*valueIfKeyNotFound=*/-1); - if (!mIsSingleVolume || streamAlias == mStreamType) { - mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index); - mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, - oldIndex); - int extraStreamType = mStreamType; - // TODO: remove this when deprecating STREAM_BLUETOOTH_SCO - if (isStreamBluetoothSco(mStreamType)) { - mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, - AudioSystem.STREAM_BLUETOOTH_SCO); - extraStreamType = AudioSystem.STREAM_BLUETOOTH_SCO; - } else { - mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, - mStreamType); - } - mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS, - streamAlias); - - if (mStreamType == streamAlias) { - String aliasStreamIndexesString = ""; - if (!aliasStreamIndexes.isEmpty()) { - aliasStreamIndexesString = - " aliased streams: " + aliasStreamIndexes; - } - AudioService.sVolumeLogger.enqueue(new VolChangedBroadcastEvent( - extraStreamType, aliasStreamIndexesString, index, oldIndex)); - if (extraStreamType != mStreamType) { - AudioService.sVolumeLogger.enqueue(new VolChangedBroadcastEvent( - mStreamType, aliasStreamIndexesString, index, oldIndex)); + if (changed) { + // If associated to volume group, update group cache + updateVolumeGroupIndex(device, /* forceMuteState= */ false); + + oldIndex = (oldIndex + 5) / 10; + index = (index + 5) / 10; + // log base stream changes to the event log + if (sStreamVolumeAlias.get(mStreamType, /*valueIfKeyNotFound=*/-1) + == mStreamType) { + if (caller == null) { + Log.w(TAG, "No caller for volume_changed event", new Throwable()); } + EventLogTags.writeVolumeChanged( + mStreamType, oldIndex, index, mIndexMax / 10, caller); } - sendBroadcastToAll(mVolumeChanged, mVolumeChangedOptions); - if (extraStreamType != mStreamType) { - // send multiple intents in case we merged voice call and bt sco streams - mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, - mStreamType); - // do not use the options in thid case which could discard - // the previous intent - sendBroadcastToAll(mVolumeChanged, null); + // fire changed intents for all streams, but only when the device it changed + // on + // is the current device + if ((index != oldIndex) && isCurrentDevice) { + // for single volume devices, only send the volume change broadcast + // on the alias stream + final int streamAlias = + sStreamVolumeAlias.get(mStreamType, /*valueIfKeyNotFound=*/-1); + if (!mIsSingleVolume || streamAlias == mStreamType) { + mVolumeChanged.putExtra( + AudioManager.EXTRA_VOLUME_STREAM_VALUE, index); + mVolumeChanged.putExtra( + AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex); + int extraStreamType = mStreamType; + // TODO: remove this when deprecating STREAM_BLUETOOTH_SCO + if (isStreamBluetoothSco(mStreamType)) { + mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, + AudioSystem.STREAM_BLUETOOTH_SCO); + extraStreamType = AudioSystem.STREAM_BLUETOOTH_SCO; + } else { + mVolumeChanged.putExtra( + AudioManager.EXTRA_VOLUME_STREAM_TYPE, mStreamType); + } + mVolumeChanged.putExtra( + AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS, streamAlias); + + if (mStreamType == streamAlias) { + String aliasStreamIndexesString = ""; + if (!aliasStreamIndexes.isEmpty()) { + aliasStreamIndexesString = + " aliased streams: " + aliasStreamIndexes; + } + AudioService.sVolumeLogger.enqueue( + new VolChangedBroadcastEvent(extraStreamType, + aliasStreamIndexesString, index, oldIndex)); + if (extraStreamType != mStreamType) { + AudioService.sVolumeLogger.enqueue( + new VolChangedBroadcastEvent(mStreamType, + aliasStreamIndexesString, index, oldIndex)); + } + } + sendBroadcastToAll(mVolumeChanged, mVolumeChangedOptions); + if (extraStreamType != mStreamType) { + // send multiple intents in case we merged voice call and bt sco + // streams + mVolumeChanged.putExtra( + AudioManager.EXTRA_VOLUME_STREAM_TYPE, mStreamType); + // do not use the options in thid case which could discard + // the previous intent + sendBroadcastToAll(mVolumeChanged, null); + } + } } } + return changed; } } - return changed; } public int getIndex(int device) { diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java index d412277d2605..f5284a3ed589 100644 --- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java +++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java @@ -211,4 +211,20 @@ public abstract class VirtualDeviceManagerInternal { */ public abstract @NonNull VirtualDeviceManager.VirtualDevice createVirtualDevice( @NonNull VirtualDeviceParams params); + + /** + * Returns the details of the virtual device with the given ID, if any. + * + * <p>The returned object is a read-only representation of the virtual device that expose its + * properties.</p> + * + * <p>Note that if the virtual device is closed and becomes invalid, the returned object will + * not be updated and may contain stale values. Use a {@link VirtualDeviceListener} for real + * time updates of the availability of virtual devices.</p> + * + * @return the virtual device with the requested ID, or {@code null} if no such device exists or + * it has already been closed. + */ + @Nullable + public abstract VirtualDevice getVirtualDevice(int deviceId); } diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java index c393e921d957..79af6ed9d60b 100644 --- a/services/core/java/com/android/server/content/ContentService.java +++ b/services/core/java/com/android/server/content/ContentService.java @@ -24,6 +24,8 @@ import android.accounts.AccountManagerInternal; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SpecialUsers.CanBeALL; +import android.annotation.SpecialUsers.CanBeCURRENT; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManager.RestrictionLevel; @@ -361,7 +363,8 @@ public final class ContentService extends IContentService.Stub { */ @Override public void registerContentObserver(Uri uri, boolean notifyForDescendants, - IContentObserver observer, int userHandle, int targetSdkVersion) { + IContentObserver observer, @CanBeALL @CanBeCURRENT @UserIdInt int userHandle, + int targetSdkVersion) { if (observer == null || uri == null) { throw new IllegalArgumentException("You must pass a valid uri and observer"); } @@ -1398,8 +1401,8 @@ public final class ContentService extends IContentService.Stub { } } - private int handleIncomingUser(Uri uri, int pid, int uid, int modeFlags, boolean allowNonFull, - int userId) { + private @CanBeALL @UserIdInt int handleIncomingUser(Uri uri, int pid, int uid, int modeFlags, + boolean allowNonFull, @CanBeALL @CanBeCURRENT @UserIdInt int userId) { if (userId == UserHandle.USER_CURRENT) { userId = ActivityManager.getCurrentUser(); } diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 88f5c81231b8..c41b8db1ce75 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -333,7 +333,7 @@ public class AutomaticBrightnessController { int ambientLightHorizonLong, float userLux, float userNits, DisplayManagerFlags displayManagerFlags) { mInjector = injector; - mClock = injector.createClock(displayManagerFlags.offloadControlsDozeAutoBrightness()); + mClock = injector.createClock(); mContext = context; mCallbacks = callbacks; mSensorManager = sensorManager; @@ -1402,8 +1402,7 @@ public class AutomaticBrightnessController { public void onSensorChanged(SensorEvent event) { if (mLightSensorEnabled) { // The time received from the sensor is in nano seconds, hence changing it to ms - final long time = (mDisplayManagerFlags.offloadControlsDozeAutoBrightness()) - ? TimeUnit.NANOSECONDS.toMillis(event.timestamp) : mClock.uptimeMillis(); + final long time = TimeUnit.NANOSECONDS.toMillis(event.timestamp); final float lux = event.values[0]; handleLightSensorEvent(time, lux); } @@ -1616,20 +1615,13 @@ public class AutomaticBrightnessController { } private static class RealClock implements Clock { - private final boolean mOffloadControlsDozeBrightness; - - RealClock(boolean offloadControlsDozeBrightness) { - mOffloadControlsDozeBrightness = offloadControlsDozeBrightness; - } - @Override public long uptimeMillis() { return SystemClock.uptimeMillis(); } public long getSensorEventScaleTime() { - return (mOffloadControlsDozeBrightness) - ? SystemClock.elapsedRealtime() : uptimeMillis(); + return SystemClock.elapsedRealtime(); } } @@ -1638,8 +1630,8 @@ public class AutomaticBrightnessController { return BackgroundThread.getHandler(); } - Clock createClock(boolean offloadControlsDozeBrightness) { - return new RealClock(offloadControlsDozeBrightness); + Clock createClock() { + return new RealClock(); } } } diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 872f33484951..f4daf8761e9b 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -890,10 +890,12 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // still need to let WindowManager know so it can update its own internal state for // things like display cutouts. display.getNonOverrideDisplayInfoLocked(mTempDisplayInfo); - if (!mTempNonOverrideDisplayInfo.equals(mTempDisplayInfo)) { - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_BASIC_CHANGED - | LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED; + if (!mTempNonOverrideDisplayInfo.equals(mTempDisplayInfo, + /* compareOnlyBasicChanges */ true)) { + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_BASIC_CHANGED; } + logicalDisplayEventMask + |= updateAndGetMaskForDisplayPropertyChanges(mTempNonOverrideDisplayInfo); } mLogicalDisplaysToUpdate.put(displayId, logicalDisplayEventMask); mUpdatedLogicalDisplays.put(displayId, UPDATE_STATE_UPDATED); diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java index 2c6f37448735..6510441ba28f 100644 --- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java +++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java @@ -291,8 +291,7 @@ public class DisplayBrightnessStrategySelector { void setAllowAutoBrightnessWhileDozing( DisplayManagerInternal.DisplayOffloadSession displayOffloadSession) { mAllowAutoBrightnessWhileDozing = mAllowAutoBrightnessWhileDozingConfig; - if (mDisplayManagerFlags.offloadControlsDozeAutoBrightness() - && mDisplayManagerFlags.isDisplayOffloadEnabled() + if (mDisplayManagerFlags.isDisplayOffloadEnabled() && displayOffloadSession != null) { mAllowAutoBrightnessWhileDozing &= displayOffloadSession.allowAutoBrightnessInDoze(); } diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index e4b595ab7c55..7cc178d5ff6c 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -156,11 +156,6 @@ public class DisplayManagerFlags { Flags.FLAG_DOZE_BRIGHTNESS_FLOAT, Flags::dozeBrightnessFloat); - private final FlagState mOffloadControlsDozeAutoBrightness = new FlagState( - Flags.FLAG_OFFLOAD_CONTROLS_DOZE_AUTO_BRIGHTNESS, - Flags::offloadControlsDozeAutoBrightness - ); - private final FlagState mPeakRefreshRatePhysicalLimit = new FlagState( Flags.FLAG_ENABLE_PEAK_REFRESH_RATE_PHYSICAL_LIMIT, Flags::enablePeakRefreshRatePhysicalLimit @@ -285,6 +280,11 @@ public class DisplayManagerFlags { Flags::committedStateSeparateEvent ); + private final FlagState mDelayImplicitRrRegistrationUntilRrAccessed = new FlagState( + Flags.FLAG_DELAY_IMPLICIT_RR_REGISTRATION_UNTIL_RR_ACCESSED, + Flags::delayImplicitRrRegistrationUntilRrAccessed + ); + /** * @return {@code true} if 'port' is allowed in display layout configuration file. */ @@ -440,13 +440,6 @@ public class DisplayManagerFlags { return mDozeBrightnessFloat.isEnabled(); } - /** - * @return Whether DisplayOffload should control auto-brightness in doze - */ - public boolean offloadControlsDozeAutoBrightness() { - return mOffloadControlsDozeAutoBrightness.isEnabled(); - } - public boolean isPeakRefreshRatePhysicalLimitEnabled() { return mPeakRefreshRatePhysicalLimit.isEnabled(); } @@ -598,7 +591,6 @@ public class DisplayManagerFlags { return mFramerateOverrideTriggersRrCallbacks.isEnabled(); } - /** * @return {@code true} if the flag for sending refresh rate events only for the apps in * foreground is enabled @@ -616,6 +608,13 @@ public class DisplayManagerFlags { } /** + * @return {@code true} if the flag for only explicit subscription for RR changes is enabled + */ + public boolean isDelayImplicitRrRegistrationUntilRrAccessedEnabled() { + return mDelayImplicitRrRegistrationUntilRrAccessed.isEnabled(); + } + + /** * dumps all flagstates * @param pw printWriter */ @@ -647,7 +646,6 @@ public class DisplayManagerFlags { pw.println(" " + mResolutionBackupRestore); pw.println(" " + mUseFusionProxSensor); pw.println(" " + mDozeBrightnessFloat); - pw.println(" " + mOffloadControlsDozeAutoBrightness); pw.println(" " + mPeakRefreshRatePhysicalLimit); pw.println(" " + mIgnoreAppPreferredRefreshRate); pw.println(" " + mSynthetic60hzModes); @@ -673,6 +671,7 @@ public class DisplayManagerFlags { pw.println(" " + mFramerateOverrideTriggersRrCallbacks); pw.println(" " + mRefreshRateEventForForegroundApps); pw.println(" " + mCommittedStateSeparateEvent); + pw.println(" " + mDelayImplicitRrRegistrationUntilRrAccessed); } private static class FlagState { diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index acdc0e0cf891..a0064a9f5d1d 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -255,17 +255,6 @@ flag { } flag { - name: "offload_controls_doze_auto_brightness" - namespace: "display_manager" - description: "Allows the registered DisplayOffloader to control if auto-brightness is used in doze" - bug: "327392714" - is_fixed_read_only: true - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "enable_peak_refresh_rate_physical_limit" namespace: "display_manager" description: "Flag for adding physical refresh rate limit if smooth display setting is on " @@ -485,9 +474,9 @@ flag { description: "Feature flag to trigger the RR callbacks when framerate overridding happens." bug: "390113266" is_fixed_read_only: true - metadata { - purpose: PURPOSE_BUGFIX - } + metadata { + purpose: PURPOSE_BUGFIX + } } flag { @@ -519,3 +508,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "delay_implicit_rr_registration_until_rr_accessed" + namespace: "display_manager" + description: "Feature flag for clients to subscribe to RR changes by either explicitly subscribing for refresh rate changes or request for refresh rate data" + bug: "391828526" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 7e8bb28b6a37..2af74f620c95 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -569,7 +569,8 @@ public final class DreamManagerService extends SystemService { } private void requestDreamInternal() { - if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront()) { + if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront() + && !isDozingInternal()) { return; } diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 977c029f3a29..d71c8a1056d9 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -146,11 +146,6 @@ final class InputGestureManager { KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL ), createKeyGesture( - KeyEvent.KEYCODE_N, - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES - ), - createKeyGesture( KeyEvent.KEYCODE_S, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 0e37238bcb84..c2fecf283a34 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -143,6 +143,7 @@ import com.android.internal.policy.KeyInterceptionInfo; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; import com.android.server.DisplayThread; +import com.android.server.IoThread; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.Watchdog; @@ -469,11 +470,13 @@ public class InputManagerService extends IInputManager.Stub static class Injector { private final Context mContext; private final Looper mLooper; + private final Looper mIoLooper; private final UEventManager mUEventManager; - Injector(Context context, Looper looper, UEventManager uEventManager) { + Injector(Context context, Looper looper, Looper ioLooper, UEventManager uEventManager) { mContext = context; mLooper = looper; + mIoLooper = ioLooper; mUEventManager = uEventManager; } @@ -485,6 +488,10 @@ public class InputManagerService extends IInputManager.Stub return mLooper; } + Looper getIoLooper() { + return mIoLooper; + } + UEventManager getUEventManager() { return mUEventManager; } @@ -505,8 +512,8 @@ public class InputManagerService extends IInputManager.Stub } public InputManagerService(Context context) { - this(new Injector(context, DisplayThread.get().getLooper(), new UEventManager() {}), - context.getSystemService(PermissionEnforcer.class)); + this(new Injector(context, DisplayThread.get().getLooper(), IoThread.get().getLooper(), + new UEventManager() {}), context.getSystemService(PermissionEnforcer.class)); } @VisibleForTesting @@ -532,7 +539,7 @@ public class InputManagerService extends IInputManager.Stub mStickyModifierStateController = new StickyModifierStateController(); mInputDataStore = new InputDataStore(); mKeyGestureController = new KeyGestureController(mContext, injector.getLooper(), - mInputDataStore); + injector.getIoLooper(), mInputDataStore); mKeyboardLedController = new KeyboardLedController(mContext, injector.getLooper(), mNative); mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper()); diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 5770a09e3b92..fba0b0453bfb 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -121,6 +121,7 @@ final class KeyGestureController { private final Context mContext; private final Handler mHandler; + private final Handler mIoHandler; private final int mSystemPid; private final KeyCombinationManager mKeyCombinationManager; private final SettingsObserver mSettingsObserver; @@ -171,9 +172,11 @@ final class KeyGestureController { private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled(); - KeyGestureController(Context context, Looper looper, InputDataStore inputDataStore) { + KeyGestureController(Context context, Looper looper, Looper ioLooper, + InputDataStore inputDataStore) { mContext = context; mHandler = new Handler(looper, this::handleMessage); + mIoHandler = new Handler(ioLooper, this::handleIoMessage); mSystemPid = Process.myPid(); mKeyGestureHandlerRecords = new TreeMap<>((p1, p2) -> { if (Objects.equals(p1, p2)) { @@ -458,7 +461,7 @@ final class KeyGestureController { userId = mCurrentUserId; } // Load the system user's input gestures. - mHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget(); + mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget(); } public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { @@ -1032,7 +1035,7 @@ final class KeyGestureController { synchronized (mUserLock) { mCurrentUserId = userId; } - mHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget(); + mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget(); } @MainThread @@ -1073,6 +1076,12 @@ final class KeyGestureController { AidlKeyGestureEvent event = (AidlKeyGestureEvent) msg.obj; notifyKeyGestureEvent(event); break; + } + return true; + } + + private boolean handleIoMessage(Message msg) { + switch (msg.what) { case MSG_PERSIST_CUSTOM_GESTURES: { final int userId = (Integer) msg.obj; persistInputGestures(userId); @@ -1083,7 +1092,6 @@ final class KeyGestureController { loadInputGestures(userId); break; } - } return true; } @@ -1144,7 +1152,7 @@ final class KeyGestureController { final int result = mInputGestureManager.addCustomInputGesture(userId, new InputGestureData(inputGestureData)); if (result == InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS) { - mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget(); + mIoHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget(); } return result; } @@ -1156,7 +1164,7 @@ final class KeyGestureController { final int result = mInputGestureManager.removeCustomInputGesture(userId, new InputGestureData(inputGestureData)); if (result == InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS) { - mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget(); + mIoHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget(); } return result; } @@ -1165,7 +1173,7 @@ final class KeyGestureController { public void removeAllCustomInputGestures(@UserIdInt int userId, @Nullable InputGestureData.Filter filter) { mInputGestureManager.removeAllCustomInputGestures(userId, filter); - mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget(); + mIoHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget(); } @BinderThread diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 508bc2f811e0..dfdd9e54fe2e 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -3485,7 +3485,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. || (windowPerceptible != null && windowPerceptible == perceptible)) { return; } - mFocusedWindowPerceptible.put(windowToken, windowPerceptible); + mFocusedWindowPerceptible.put(windowToken, perceptible); updateSystemUiLocked(userId); } }); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index 2ea61171b9e1..940bcb4c6ba1 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -16,6 +16,7 @@ package com.android.server.location.contexthub; +import android.annotation.NonNull; import android.app.AppOpsManager; import android.content.Context; import android.hardware.contexthub.EndpointInfo; @@ -64,7 +65,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub * Internal interface used to invoke client callbacks. */ interface CallbackConsumer { - void accept(IContextHubEndpointCallback callback) throws RemoteException; + void accept(@NonNull IContextHubEndpointCallback callback) throws RemoteException; } /** The context of the service. */ @@ -86,7 +87,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private final EndpointInfo mHalEndpointInfo; /** The remote callback interface for this endpoint. */ - private final IContextHubEndpointCallback mContextHubEndpointCallback; + @NonNull private final IContextHubEndpointCallback mContextHubEndpointCallback; /** True if this endpoint is registered with the service/HAL. */ @GuardedBy("mRegistrationLock") @@ -158,7 +159,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub IEndpointCommunication hubInterface, ContextHubEndpointManager endpointManager, EndpointInfo halEndpointInfo, - IContextHubEndpointCallback callback, + @NonNull IContextHubEndpointCallback callback, String packageName, String attributionTag, ContextHubTransactionManager transactionManager) { @@ -374,6 +375,34 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(mEndpointInfo).append(", "); + sb.append("package: ").append(mPackageName).append(", "); + synchronized (mWakeLock) { + sb.append("wakelock: ").append(mWakeLock); + } + synchronized (mOpenSessionLock) { + if (mSessionInfoMap.size() != 0) { + sb.append(System.lineSeparator()); + sb.append(" sessions: "); + sb.append(System.lineSeparator()); + } + for (int i = 0; i < mSessionInfoMap.size(); i++) { + int id = mSessionInfoMap.keyAt(i); + int count = i + 1; + sb.append( + " " + count + ". id=" + + id + + ", remote:" + + mSessionInfoMap.get(id).getRemoteEndpointInfo()); + sb.append(System.lineSeparator()); + } + } + return sb.toString(); + } + /** * Registers this endpoints with the Context Hub HAL. * @@ -391,9 +420,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } /* package */ void attachDeathRecipient() throws RemoteException { - if (mContextHubEndpointCallback != null) { - mContextHubEndpointCallback.asBinder().linkToDeath(this, 0 /* flags */); - } + mContextHubEndpointCallback.asBinder().linkToDeath(this, 0 /* flags */); } /* package */ void onEndpointSessionOpenRequest( @@ -636,15 +663,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub * @return false if the callback threw a RemoteException */ private boolean invokeCallback(CallbackConsumer consumer) { - if (mContextHubEndpointCallback != null) { - acquireWakeLock(); - try { - consumer.accept(mContextHubEndpointCallback); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling endpoint callback", e); - releaseWakeLock(); - return false; - } + acquireWakeLock(); + try { + consumer.accept(mContextHubEndpointCallback); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException while calling endpoint callback", e); + releaseWakeLock(); + return false; } return true; } diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java index 06aeb62a28b8..8ab581e1fb7a 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java @@ -16,6 +16,8 @@ package com.android.server.location.contexthub; +import android.annotation.IntDef; +import android.annotation.NonNull; import android.content.Context; import android.hardware.contexthub.ContextHubInfo; import android.hardware.contexthub.EndpointInfo; @@ -34,8 +36,11 @@ import android.util.Log; 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.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -105,6 +110,48 @@ import java.util.function.Consumer; /** The interface for endpoint communication (retrieved from HAL in init()) */ private IEndpointCommunication mHubInterface = null; + /* + * The list of previous registration records. + */ + private static final int NUM_CLIENT_RECORDS = 20; + private final ConcurrentLinkedEvictingDeque<RegistrationRecord> mRegistrationRecordDeque = + new ConcurrentLinkedEvictingDeque<>(NUM_CLIENT_RECORDS); + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"ACTION_"}, + value = { + ACTION_REGISTERED, + ACTION_UNREGISTERED, + }) + public @interface Action {} + + public static final int ACTION_REGISTERED = 0; + public static final int ACTION_UNREGISTERED = 1; + + /** A container class to store a record of ContextHubEndpointBroker registrations. */ + private class RegistrationRecord { + private final String mBroker; + private final int mAction; + private final long mTimestamp; + + RegistrationRecord(String broker, @Action int action) { + mBroker = broker; + mAction = action; + mTimestamp = System.currentTimeMillis(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(ContextHubServiceUtil.formatDateFromTimestamp(mTimestamp)); + sb.append(" "); + sb.append(mAction == ACTION_REGISTERED ? "+ " : "- "); + sb.append(mBroker); + return sb.toString(); + } + } + /* package */ ContextHubEndpointManager( Context context, IContextHubWrapper contextHubProxy, @@ -194,7 +241,7 @@ import java.util.function.Consumer; */ /* package */ IContextHubEndpoint registerEndpoint( HubEndpointInfo pendingEndpointInfo, - IContextHubEndpointCallback callback, + @NonNull IContextHubEndpointCallback callback, String packageName, String attributionTag) throws RemoteException { @@ -228,6 +275,7 @@ import java.util.function.Consumer; return null; } + mRegistrationRecordDeque.add(new RegistrationRecord(broker.toString(), ACTION_REGISTERED)); Log.d(TAG, "Registered endpoint with ID = " + endpointId); return IContextHubEndpoint.Stub.asInterface(broker); } @@ -274,7 +322,11 @@ import java.util.function.Consumer; * @param endpointId The ID of the endpoint to unregister. */ /* package */ void unregisterEndpoint(long endpointId) { - mEndpointMap.remove(endpointId); + ContextHubEndpointBroker broker = mEndpointMap.remove(endpointId); + if (broker != null) { + mRegistrationRecordDeque.add( + new RegistrationRecord(broker.toString(), ACTION_UNREGISTERED)); + } } /** Invoked by the service when the Context Hub HAL restarts. */ @@ -366,6 +418,27 @@ import java.util.function.Consumer; } } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + int count = 1; + for (ContextHubEndpointBroker broker : mEndpointMap.values()) { + sb.append(count + ". " + broker); + sb.append(System.lineSeparator()); + count++; + } + + sb.append(System.lineSeparator()); + sb.append("Registration History:"); + sb.append(System.lineSeparator()); + Iterator<RegistrationRecord> it = mRegistrationRecordDeque.descendingIterator(); + while (it.hasNext()) { + sb.append(it.next()); + sb.append(System.lineSeparator()); + } + return sb.toString(); + } + /** * Invokes a callback for a session with matching ID. * diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java index 502a7aeba258..2c0c55bd8df4 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java @@ -792,6 +792,10 @@ public class ContextHubService extends IContextHubService.Stub { Log.e(TAG, "Endpoint manager failed to initialize"); throw new UnsupportedOperationException("Endpoint registration is not supported"); } + if (callback == null) { + Log.e(TAG, "Endpoint callback is invalid"); + throw new IllegalArgumentException("registerEndpoint must have a non-null callback"); + } return mEndpointManager.registerEndpoint( pendingHubEndpointInfo, callback, packageName, attributionTag); } @@ -1579,6 +1583,12 @@ public class ContextHubService extends IContextHubService.Stub { pw.println("=================== CLIENTS ===================="); pw.println(mClientManager); + if (mEndpointManager != null) { + pw.println(""); + pw.println("=================== ENDPOINTS ===================="); + pw.println(mEndpointManager); + } + pw.println(""); pw.println("=================== TRANSACTIONS ===================="); pw.println(mTransactionManager); diff --git a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java index 177eefb2ef2a..3f75b11befc2 100644 --- a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java +++ b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java @@ -29,7 +29,6 @@ import android.telephony.TelephonyManager; import android.util.Log; import com.android.internal.telephony.TelephonyIntents; -import com.android.internal.telephony.flags.Flags; import com.android.server.FgThread; import java.util.Objects; @@ -107,26 +106,19 @@ public class SystemEmergencyHelper extends EmergencyHelper { boolean isInExtensionTime = mEmergencyCallEndRealtimeMs != Long.MIN_VALUE && (SystemClock.elapsedRealtime() - mEmergencyCallEndRealtimeMs) < extensionTimeMs; - if (!Flags.enforceTelephonyFeatureMapping()) { - return mIsInEmergencyCall - || isInExtensionTime - || mTelephonyManager.getEmergencyCallbackMode() - || mTelephonyManager.isInEmergencySmsMode(); - } else { - boolean emergencyCallbackMode = false; - boolean emergencySmsMode = false; - PackageManager pm = mContext.getPackageManager(); - if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) { - emergencyCallbackMode = mTelephonyManager.getEmergencyCallbackMode(); - } - if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) { - emergencySmsMode = mTelephonyManager.isInEmergencySmsMode(); - } - return mIsInEmergencyCall - || isInExtensionTime - || emergencyCallbackMode - || emergencySmsMode; + boolean emergencyCallbackMode = false; + boolean emergencySmsMode = false; + PackageManager pm = mContext.getPackageManager(); + if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) { + emergencyCallbackMode = mTelephonyManager.getEmergencyCallbackMode(); + } + if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) { + emergencySmsMode = mTelephonyManager.isInEmergencySmsMode(); } + return mIsInEmergencyCall + || isInExtensionTime + || emergencyCallbackMode + || emergencySmsMode; } private class EmergencyCallTelephonyCallback extends TelephonyCallback implements diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/OWNERS b/services/core/java/com/android/server/locksettings/recoverablekeystore/OWNERS index bb487fb52c9f..ebf7e6bed064 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/OWNERS +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/OWNERS @@ -1,4 +1,3 @@ aseemk@google.com bozhu@google.com dementyev@google.com -robertberry@google.com diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 0fc182f3f1bb..fff812c038e7 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -42,6 +42,7 @@ import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_P import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED; +import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_BUBBLE; import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE; import android.annotation.FlaggedApi; @@ -286,7 +287,7 @@ public class PreferencesHelper implements RankingConfig { if (!TAG_RANKING.equals(tag)) return; final int xmlVersion = parser.getAttributeInt(null, ATT_VERSION, -1); - boolean upgradeForBubbles = xmlVersion == XML_VERSION_BUBBLES_UPGRADE; + boolean upgradeForBubbles = xmlVersion >= XML_VERSION_BUBBLES_UPGRADE; boolean migrateToPermission = (xmlVersion < XML_VERSION_NOTIF_PERMISSION); if (mShowReviewPermissionsNotification && (xmlVersion < XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION)) { @@ -337,15 +338,19 @@ public class PreferencesHelper implements RankingConfig { } boolean skipWarningLogged = false; boolean skipGroupWarningLogged = false; - boolean hasSAWPermission = false; - if (upgradeForBubbles && uid != UNKNOWN_UID) { - hasSAWPermission = mAppOps.noteOpNoThrow( - OP_SYSTEM_ALERT_WINDOW, uid, name, null, - "check-notif-bubble") == AppOpsManager.MODE_ALLOWED; - } - int bubblePref = hasSAWPermission - ? BUBBLE_PREFERENCE_ALL - : parser.getAttributeInt(null, ATT_ALLOW_BUBBLE, DEFAULT_BUBBLE_PREFERENCE); + int bubblePref = parser.getAttributeInt(null, ATT_ALLOW_BUBBLE, + DEFAULT_BUBBLE_PREFERENCE); + boolean bubbleLocked = (parser.getAttributeInt(null, + ATT_APP_USER_LOCKED_FIELDS, DEFAULT_LOCKED_APP_FIELDS) & USER_LOCKED_BUBBLE) + != 0; + if (!bubbleLocked + && upgradeForBubbles + && uid != UNKNOWN_UID + && mAppOps.noteOpNoThrow(OP_SYSTEM_ALERT_WINDOW, uid, name, null, + "check-notif-bubble") == AppOpsManager.MODE_ALLOWED) { + // User hasn't changed bubble pref & the app has SAW, so allow all bubbles. + bubblePref = BUBBLE_PREFERENCE_ALL; + } int appImportance = parser.getAttributeInt(null, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); // when data is loaded from disk it's loaded as USER_ALL, but restored data that diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java index 29f8243cfe60..ae415196f15e 100644 --- a/services/core/java/com/android/server/pm/AppDataHelper.java +++ b/services/core/java/com/android/server/pm/AppDataHelper.java @@ -24,6 +24,7 @@ import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; import android.annotation.UserIdInt; import android.content.pm.PackageManager; import android.os.CreateAppDataArgs; @@ -548,7 +549,7 @@ public class AppDataHelper { return prepareAppDataFuture; } - void clearAppDataLIF(AndroidPackage pkg, int userId, int flags) { + void clearAppDataLIF(AndroidPackage pkg, @CanBeALL @UserIdInt int userId, int flags) { if (pkg == null) { return; } @@ -559,7 +560,8 @@ public class AppDataHelper { } } - void clearAppDataLeafLIF(String packageName, String volumeUuid, int userId, int flags) { + void clearAppDataLeafLIF(String packageName, String volumeUuid, @CanBeALL @UserIdInt int userId, + int flags) { final Computer snapshot = mPm.snapshotComputer(); final PackageStateInternal packageStateInternal = snapshot.getPackageStateInternal(packageName); diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index c3af578de369..463989adc98f 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -332,7 +332,8 @@ public class BackgroundInstallControlService extends SystemService { userId) != PERMISSION_GRANTED) { if(Build.IS_DEBUGGABLE) { - Slog.d(TAG, "handlePackageAdd " + packageName + ": installer doesn't " + Slog.d(TAG, "handlePackageAdd " + packageName + ": installer (" + + installerPackageName + ") doesn't " + "have INSTALL_PACKAGES permission, skipping"); } return; diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java index 90adb6683496..6bec34ef7063 100644 --- a/services/core/java/com/android/server/pm/DeletePackageHelper.java +++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java @@ -35,6 +35,8 @@ import static com.android.server.pm.PackageManagerService.TAG; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; +import android.annotation.UserIdInt; import android.app.ApplicationExitInfo; import android.app.ApplicationPackageManager; import android.content.Intent; @@ -562,7 +564,7 @@ final class DeletePackageHelper { } @GuardedBy("mPm.mInstallLock") - private void deleteInstalledPackageLIF(PackageSetting ps, int userId, + private void deleteInstalledPackageLIF(PackageSetting ps, @CanBeALL @UserIdInt int userId, boolean deleteCodeAndResources, int flags, @NonNull int[] allUserHandles, @NonNull PackageRemovedInfo outInfo, boolean writeSettings) { synchronized (mPm.mLock) { diff --git a/services/core/java/com/android/server/pm/PackageFreezer.java b/services/core/java/com/android/server/pm/PackageFreezer.java index 11f2059c4267..d66eb814ef66 100644 --- a/services/core/java/com/android/server/pm/PackageFreezer.java +++ b/services/core/java/com/android/server/pm/PackageFreezer.java @@ -18,6 +18,8 @@ package com.android.server.pm; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; +import android.annotation.UserIdInt; import android.content.pm.Flags; import android.content.pm.PackageManager; @@ -60,12 +62,12 @@ final class PackageFreezer implements AutoCloseable { } } - PackageFreezer(String packageName, int userId, String killReason, + PackageFreezer(String packageName, @CanBeALL @UserIdInt int userId, String killReason, PackageManagerService pm, int exitInfoReason, @Nullable InstallRequest request) { this(packageName, userId, killReason, pm, exitInfoReason, request, false); } - PackageFreezer(String packageName, int userId, String killReason, + PackageFreezer(String packageName, @CanBeALL @UserIdInt int userId, String killReason, PackageManagerService pm, int exitInfoReason, @Nullable InstallRequest request, boolean waitAppKilled) { mPm = pm; diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 352985d5a023..136cb1259113 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -948,6 +948,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { break; case MSG_ON_NATIVE_LIBS_EXTRACTED: handleOnNativeLibsExtracted(); + break; } return true; diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 2464a291b4dd..91a1c9c12cb8 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -50,6 +50,7 @@ import android.annotation.AppIdInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; import android.annotation.StringRes; import android.annotation.UserIdInt; import android.annotation.WorkerThread; @@ -1590,7 +1591,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService scheduleWritePackageRestrictions(userId); } - void scheduleWritePackageRestrictions(int userId) { + void scheduleWritePackageRestrictions(@CanBeALL @UserIdInt int userId) { invalidatePackageInfoCache(); if (userId == UserHandle.USER_ALL) { synchronized (mDirtyUsers) { @@ -3074,7 +3075,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService } @NonNull - int[] resolveUserIds(int userId) { + int[] resolveUserIds(@CanBeALL @UserIdInt int userId) { return (userId == UserHandle.USER_ALL) ? mUserManager.getUserIds() : new int[] { userId }; } @@ -3112,7 +3113,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService } void killApplication(String pkgName, @AppIdInt int appId, - @UserIdInt int userId, String reason, int exitInfoReason) { + @CanBeALL @UserIdInt int userId, String reason, int exitInfoReason) { // Request the ActivityManager to kill the process(only for existing packages) // so that we do not end up in a confused state while the user is still using the older // version of the application while the new one gets installed. @@ -3131,7 +3132,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService } void killApplicationSync(String pkgName, @AppIdInt int appId, - @UserIdInt int userId, String reason, int exitInfoReason) { + @CanBeALL @UserIdInt int userId, String reason, int exitInfoReason) { ActivityManagerInternal mAmi = LocalServices.getService(ActivityManagerInternal.class); if (Thread.holdsLock(mLock) || mAmi == null) { // holds PM's lock, go back killApplication to avoid it run into watchdog reset. @@ -3385,7 +3386,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService } // TODO(b/261957226): centralise this logic in DPM - boolean isPackageDeviceAdmin(String packageName, int userId) { + boolean isPackageDeviceAdmin(String packageName, @CanBeALL @UserIdInt int userId) { final IDevicePolicyManager dpm = getDevicePolicyManager(); final DevicePolicyManagerInternal dpmi = mInjector.getLocalService(DevicePolicyManagerInternal.class); @@ -3555,7 +3556,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService /** This method takes a specific user id as well as UserHandle.USER_ALL. */ @GuardedBy("mLock") void clearPackagePreferredActivitiesLPw(String packageName, - @NonNull SparseBooleanArray outUserChanged, int userId) { + @NonNull SparseBooleanArray outUserChanged, @CanBeALL @UserIdInt int userId) { mSettings.clearPackagePreferredActivities(packageName, outUserChanged, userId); } @@ -4388,14 +4389,14 @@ public class PackageManagerService implements PackageSender, TestUtilityService } } - public PackageFreezer freezePackage(String packageName, int userId, String killReason, - int exitInfoReason, InstallRequest request) { + public PackageFreezer freezePackage(String packageName, @CanBeALL @UserIdInt int userId, + String killReason, int exitInfoReason, InstallRequest request) { return freezePackage(packageName, userId, killReason, exitInfoReason, request, /* waitAppKilled= */ false); } - private PackageFreezer freezePackage(String packageName, int userId, String killReason, - int exitInfoReason, InstallRequest request, boolean waitAppKilled) { + private PackageFreezer freezePackage(String packageName, @CanBeALL @UserIdInt int userId, + String killReason, int exitInfoReason, InstallRequest request, boolean waitAppKilled) { return new PackageFreezer(packageName, userId, killReason, this, exitInfoReason, request, waitAppKilled); } diff --git a/services/core/java/com/android/server/pm/PreferredActivityHelper.java b/services/core/java/com/android/server/pm/PreferredActivityHelper.java index 41d2aeb9b168..fa56596bf62d 100644 --- a/services/core/java/com/android/server/pm/PreferredActivityHelper.java +++ b/services/core/java/com/android/server/pm/PreferredActivityHelper.java @@ -25,6 +25,7 @@ import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED; import static com.android.server.pm.PackageManagerService.TAG; import android.annotation.NonNull; +import android.annotation.SpecialUsers.CanBeALL; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Intent; @@ -115,7 +116,8 @@ final class PreferredActivityHelper { } /** This method takes a specific user id as well as UserHandle.USER_ALL. */ - public void clearPackagePreferredActivities(String packageName, int userId) { + public void clearPackagePreferredActivities(String packageName, + @CanBeALL @UserIdInt int userId) { final SparseBooleanArray changedUsers = new SparseBooleanArray(); synchronized (mPm.mLock) { mPm.clearPackagePreferredActivitiesLPw(packageName, changedUsers, userId); diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index f01a74e8d60d..22b4ec7b51b6 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -30,6 +30,8 @@ import static com.android.server.pm.PackageManagerService.TAG; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; +import android.annotation.UserIdInt; import android.content.pm.PackageManager; import android.content.pm.parsing.ApkLiteParseUtils; import android.content.pm.parsing.PackageLite; @@ -256,7 +258,8 @@ final class RemovePackageHelper { * Make sure this flag is set for partially installed apps. If not it's meaningless to * delete a partially installed application. */ - public void clearPackageStateForUserLIF(PackageSetting ps, int userId, int flags) { + public void clearPackageStateForUserLIF(PackageSetting ps, @CanBeALL @UserIdInt int userId, + int flags) { final String packageName = ps.getPackageName(); // Step 1: always destroy app profiles except when explicitly preserved if ((flags & Installer.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES) == 0) { @@ -370,13 +373,13 @@ final class RemovePackageHelper { * This method deletes the package from internal data structures such as mPackages / mSettings. * * @param targetUserId indicates the target user of the deletion. It equals to - * {@link UserHandle.USER_ALL} if the deletion was initiated for all users, + * {@link UserHandle#USER_ALL} if the deletion was initiated for all users, * otherwise it equals to the specific user id that the deletion was meant * for. */ @GuardedBy("mPm.mInstallLock") - public void removePackageDataLIF(final PackageSetting deletedPs, int targetUserId, - @NonNull int[] allUserHandles, + public void removePackageDataLIF(final PackageSetting deletedPs, + @CanBeALL @UserIdInt int targetUserId, @NonNull int[] allUserHandles, @NonNull PackageRemovedInfo outInfo, int flags, boolean writeSettings) { String packageName = deletedPs.getPackageName(); if (DEBUG_REMOVE) Slog.d(TAG, "removePackageDataLI: " + deletedPs); @@ -482,7 +485,8 @@ final class RemovePackageHelper { } } - private static boolean shouldDeletePackageSetting(PackageSetting deletedPs, int userId, + private static boolean shouldDeletePackageSetting(PackageSetting deletedPs, + @CanBeALL @UserIdInt int userId, int[] allUserHandles, int flags) { if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) { return false; diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 485a28070bc5..92257f1ee2dd 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -35,6 +35,7 @@ import static com.android.server.pm.SharedUidMigration.BEST_EFFORT; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; import android.annotation.UserIdInt; import android.app.compat.ChangeIdStateCache; import android.content.ComponentName; @@ -6639,7 +6640,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile /** This method takes a specific user id as well as UserHandle.USER_ALL. */ void clearPackagePreferredActivities(String packageName, - @NonNull SparseBooleanArray outUserChanged, int userId) { + @NonNull SparseBooleanArray outUserChanged, @CanBeALL @UserIdInt int userId) { boolean changed = false; ArrayList<PreferredActivity> removed = null; for (int i = 0; i < mPreferredActivities.size(); i++) { diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java index 045d4db0a1f1..346327d1fa74 100644 --- a/services/core/java/com/android/server/pm/ShortcutLauncher.java +++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java @@ -453,7 +453,7 @@ class ShortcutLauncher extends ShortcutPackageItem { @Override protected File getShortcutPackageItemFile() { final File path = new File(mShortcutUser.mService.injectUserDataPath( - mShortcutUser.getUserId()), ShortcutUser.DIRECTORY_LUANCHERS); + mShortcutUser.getUserId()), ShortcutUser.DIRECTORY_LAUNCHERS); // Package user id and owner id can have different values for ShortcutLaunchers. Adding // user Id to the file name to create a unique path. Owner id is used in the root path. final String fileName = getPackageName() + getPackageUserId() + ".xml"; diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 373c1ed3c386..d3513053caf3 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -16,7 +16,9 @@ package com.android.server.pm; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; -import static android.provider.DeviceConfig.NAMESPACE_SYSTEMUI; + +import static com.android.server.pm.ShortcutUser.DIRECTORY_LAUNCHERS; +import static com.android.server.pm.ShortcutUser.DIRECTORY_PACKAGES; import android.Manifest.permission; import android.annotation.IntDef; @@ -94,7 +96,6 @@ import android.os.ShellCommand; import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; -import android.provider.DeviceConfig; import android.text.TextUtils; import android.text.format.TimeMigrationUtils; import android.util.ArraySet; @@ -112,7 +113,6 @@ import android.view.IWindowManager; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.MetricsLogger; import com.android.internal.util.CollectionUtils; import com.android.internal.util.DumpUtils; @@ -155,7 +155,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Pattern; -import java.util.stream.Collectors; /** * TODO: @@ -171,7 +170,7 @@ public class ShortcutService extends IShortcutService.Stub { static final boolean DEBUG = false; // STOPSHIP if true static final boolean DEBUG_LOAD = false; // STOPSHIP if true static final boolean DEBUG_PROCSTATE = false; // STOPSHIP if true - static final boolean DEBUG_REBOOT = Build.IS_DEBUGGABLE; + static final boolean DEBUG_REBOOT = false; // STOPSHIP if true @VisibleForTesting static final long DEFAULT_RESET_INTERVAL_SEC = 24 * 60 * 60; // 1 day @@ -292,7 +291,8 @@ public class ShortcutService extends IShortcutService.Stub { final Context mContext; - private final Object mServiceLock = new Object(); + @VisibleForTesting + final Object mServiceLock = new Object(); private final Object mNonPersistentUsersLock = new Object(); private final Object mWtfLock = new Object(); @@ -982,7 +982,7 @@ public class ShortcutService extends IShortcutService.Stub { } @VisibleForTesting - void saveBaseState() { + void injectSaveBaseState() { try (ResilientAtomicFile file = getBaseStateFile()) { if (DEBUG || DEBUG_REBOOT) { Slog.d(TAG, "Saving to " + file.getBaseFile()); @@ -994,18 +994,7 @@ public class ShortcutService extends IShortcutService.Stub { outs = file.startWrite(); } - // Write to XML - TypedXmlSerializer out = Xml.resolveSerializer(outs); - out.startDocument(null, true); - out.startTag(null, TAG_ROOT); - - // Body. - // No locking required. Ok to add lock later if we save more data. - writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime.get()); - - // Epilogue. - out.endTag(null, TAG_ROOT); - out.endDocument(); + saveBaseStateAsXml(outs); // Close. injectFinishWrite(file, outs); @@ -1016,10 +1005,32 @@ public class ShortcutService extends IShortcutService.Stub { } } + @VisibleForTesting + protected void saveBaseStateAsXml(OutputStream outs) throws IOException { + // Write to XML + TypedXmlSerializer out = Xml.resolveSerializer(outs); + out.startDocument(null, true); + out.startTag(null, TAG_ROOT); + + // Body. + // No locking required. Ok to add lock later if we save more data. + writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime.get()); + + // Epilogue. + out.endTag(null, TAG_ROOT); + out.endDocument(); + } + @GuardedBy("mServiceLock") private void loadBaseStateLocked() { mRawLastResetTime.set(0); + injectLoadBaseState(); + // Adjust the last reset time. + getLastResetTimeLocked(); + } + @VisibleForTesting + protected void injectLoadBaseState() { try (ResilientAtomicFile file = getBaseStateFile()) { if (DEBUG || DEBUG_REBOOT) { Slog.d(TAG, "Loading from " + file.getBaseFile()); @@ -1030,34 +1041,7 @@ public class ShortcutService extends IShortcutService.Stub { if (in == null) { throw new FileNotFoundException(file.getBaseFile().getAbsolutePath()); } - - TypedXmlPullParser parser = Xml.resolvePullParser(in); - - int type; - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { - if (type != XmlPullParser.START_TAG) { - continue; - } - final int depth = parser.getDepth(); - // Check the root tag - final String tag = parser.getName(); - if (depth == 1) { - if (!TAG_ROOT.equals(tag)) { - Slog.v(TAG, "Invalid root tag: " + tag); - return; - } - continue; - } - // Assume depth == 2 - switch (tag) { - case TAG_LAST_RESET_TIME: - mRawLastResetTime.set(parseLongAttribute(parser, ATTR_VALUE)); - break; - default: - Slog.v(TAG, "Invalid tag: " + tag); - break; - } - } + loadBaseStateAsXml(in); } catch (FileNotFoundException e) { // Use the default } catch (IOException | XmlPullParserException e) { @@ -1067,8 +1051,38 @@ public class ShortcutService extends IShortcutService.Stub { return; } } - // Adjust the last reset time. - getLastResetTimeLocked(); + } + + @VisibleForTesting + protected void loadBaseStateAsXml(InputStream in) + throws IOException, XmlPullParserException { + TypedXmlPullParser parser = Xml.resolvePullParser(in); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final int depth = parser.getDepth(); + // Check the root tag + final String tag = parser.getName(); + if (depth == 1) { + if (!TAG_ROOT.equals(tag)) { + Slog.v(TAG, "Invalid root tag: " + tag); + return; + } + continue; + } + // Assume depth == 2 + switch (tag) { + case TAG_LAST_RESET_TIME: + mRawLastResetTime.set(parseLongAttribute(parser, ATTR_VALUE)); + break; + default: + Slog.v(TAG, "Invalid tag: " + tag); + break; + } + } } @VisibleForTesting @@ -1083,7 +1097,8 @@ public class ShortcutService extends IShortcutService.Stub { "user shortcut", null); } - private void saveUser(@UserIdInt int userId) { + @VisibleForTesting + protected void injectSaveUser(@UserIdInt int userId) { try (ResilientAtomicFile file = getUserFile(userId)) { FileOutputStream os = null; try { @@ -1092,8 +1107,14 @@ public class ShortcutService extends IShortcutService.Stub { } synchronized (mServiceLock) { + // Since we are not handling package deletion yet, or any single package + // changes, just clean the directory and rewrite all the ShortcutPackageItems. + final File root = injectUserDataPath(userId); + FileUtils.deleteContents(new File(root, DIRECTORY_PACKAGES)); + FileUtils.deleteContents(new File(root, DIRECTORY_LAUNCHERS)); os = file.startWrite(); saveUserInternalLocked(userId, os, /* forBackup= */ false); + getUserShortcutsLocked(userId).scheduleSaveAllLaunchersAndPackages(); } injectFinishWrite(file, os); @@ -1109,8 +1130,9 @@ public class ShortcutService extends IShortcutService.Stub { getUserShortcutsLocked(userId).logSharingShortcutStats(mMetricsLogger); } + @VisibleForTesting @GuardedBy("mServiceLock") - private void saveUserInternalLocked(@UserIdInt int userId, OutputStream os, + protected void saveUserInternalLocked(@UserIdInt int userId, OutputStream os, boolean forBackup) throws IOException, XmlPullParserException { // Write to XML @@ -1138,8 +1160,9 @@ public class ShortcutService extends IShortcutService.Stub { Slog.w(TAG, String.format("Invalid tag '%s' found at depth %d", tag, depth)); } + @VisibleForTesting @Nullable - private ShortcutUser loadUserLocked(@UserIdInt int userId) { + protected ShortcutUser injectLoadUserLocked(@UserIdInt int userId) { try (ResilientAtomicFile file = getUserFile(userId)) { FileInputStream in = null; try { @@ -1157,12 +1180,13 @@ public class ShortcutService extends IShortcutService.Stub { } catch (Exception e) { // Remove corrupted file and retry. file.failRead(in, e); - return loadUserLocked(userId); + return injectLoadUserLocked(userId); } } } - private ShortcutUser loadUserInternal(@UserIdInt int userId, InputStream is, + @VisibleForTesting + protected ShortcutUser loadUserInternal(@UserIdInt int userId, InputStream is, boolean fromBackup) throws XmlPullParserException, IOException, InvalidFileFormatException { @@ -1240,9 +1264,9 @@ public class ShortcutService extends IShortcutService.Stub { for (int i = dirtyUserIds.size() - 1; i >= 0; i--) { final int userId = dirtyUserIds.get(i); if (userId == UserHandle.USER_NULL) { // USER_NULL for base state. - saveBaseState(); + injectSaveBaseState(); } else { - saveUser(userId); + injectSaveUser(userId); } } } catch (Exception e) { @@ -1349,7 +1373,7 @@ public class ShortcutService extends IShortcutService.Stub { ShortcutUser userPackages = mUsers.get(userId); if (userPackages == null) { - userPackages = loadUserLocked(userId); + userPackages = injectLoadUserLocked(userId); if (userPackages == null) { userPackages = new ShortcutUser(this, userId); } @@ -1430,8 +1454,9 @@ public class ShortcutService extends IShortcutService.Stub { * {@link ShortcutBitmapSaver#waitForAllSavesLocked()} to make sure there's no pending bitmap * saves are going on. */ + @VisibleForTesting @GuardedBy("mServiceLock") - private void cleanupDanglingBitmapDirectoriesLocked(@UserIdInt int userId) { + void cleanupDanglingBitmapDirectoriesLocked(@UserIdInt int userId) { if (DEBUG) { Slog.d(TAG, "cleanupDanglingBitmaps: userId=" + userId); } @@ -2755,7 +2780,7 @@ public class ShortcutService extends IShortcutService.Stub { getPackageShortcutsLocked(packageName, userId) .resetRateLimitingForCommandLineNoSaving(); } - saveUser(userId); + injectSaveUser(userId); } // We override this method in unit tests to do a simpler check. @@ -4407,7 +4432,7 @@ public class ShortcutService extends IShortcutService.Stub { pw.println(); }); } - saveUser(userId); + injectSaveUser(userId); } // === Dump === diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java index bc8cc7ba29af..632fd16a32f7 100644 --- a/services/core/java/com/android/server/pm/ShortcutUser.java +++ b/services/core/java/com/android/server/pm/ShortcutUser.java @@ -21,16 +21,12 @@ import android.annotation.UserIdInt; import android.content.pm.ShortcutManager; import android.content.pm.UserPackage; import android.metrics.LogMaker; -import android.os.Binder; -import android.os.FileUtils; -import android.os.UserHandle; import android.text.TextUtils; import android.text.format.Formatter; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; -import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -49,8 +45,6 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -63,7 +57,7 @@ class ShortcutUser { private static final String TAG = ShortcutService.TAG; static final String DIRECTORY_PACKAGES = "packages"; - static final String DIRECTORY_LUANCHERS = "launchers"; + static final String DIRECTORY_LAUNCHERS = "launchers"; static final String TAG_ROOT = "user"; private static final String TAG_LAUNCHER = "launcher"; @@ -322,41 +316,43 @@ class ShortcutUser { mService.injectBuildFingerprint()); } - if (!forBackup) { - // Since we are not handling package deletion yet, or any single package changes, just - // clean the directory and rewrite all the ShortcutPackageItems. - final File root = mService.injectUserDataPath(mUserId); - FileUtils.deleteContents(new File(root, DIRECTORY_PACKAGES)); - FileUtils.deleteContents(new File(root, DIRECTORY_LUANCHERS)); - } // Can't use forEachPackageItem due to the checked exceptions. + if (forBackup) { + int size = mLaunchers.size(); + for (int i = 0; i < size; i++) { + saveShortcutPackageItem(out, mLaunchers.valueAt(i)); + } + size = mPackages.size(); + for (int i = 0; i < size; i++) { + saveShortcutPackageItem(out, mPackages.valueAt(i)); + } + } + + out.endTag(null, TAG_ROOT); + } + + void scheduleSaveAllLaunchersAndPackages() { { final int size = mLaunchers.size(); for (int i = 0; i < size; i++) { - saveShortcutPackageItem(out, mLaunchers.valueAt(i), forBackup); + mLaunchers.valueAt(i).scheduleSave(); } } { final int size = mPackages.size(); for (int i = 0; i < size; i++) { - saveShortcutPackageItem(out, mPackages.valueAt(i), forBackup); + mPackages.valueAt(i).scheduleSave(); } } - - out.endTag(null, TAG_ROOT); } - private void saveShortcutPackageItem(TypedXmlSerializer out, ShortcutPackageItem spi, - boolean forBackup) throws IOException, XmlPullParserException { - if (forBackup) { - if (spi.getPackageUserId() != spi.getOwnerUserId()) { - return; // Don't save cross-user information. - } - spi.waitForBitmapSaves(); - spi.saveToXml(out, forBackup); - } else { - spi.scheduleSave(); + private void saveShortcutPackageItem(TypedXmlSerializer out, ShortcutPackageItem spi) + throws IOException, XmlPullParserException { + if (spi.getPackageUserId() != spi.getOwnerUserId()) { + return; // Don't save cross-user information. } + spi.waitForBitmapSaves(); + spi.saveToXml(out, true /* forBackup */); } public static ShortcutUser loadFromXml(ShortcutService s, TypedXmlPullParser parser, int userId, @@ -429,7 +425,7 @@ class ShortcutUser { } }); - forMainFilesIn(new File(root, DIRECTORY_LUANCHERS), (File f) -> { + forMainFilesIn(new File(root, DIRECTORY_LAUNCHERS), (File f) -> { final ShortcutLauncher sl = ShortcutLauncher.loadFromFile(f, ret, userId, fromBackup); if (sl != null) { diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 5c5a9c1b6c05..ac19ea12c6a4 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -37,6 +37,7 @@ import android.Manifest; import android.annotation.AppIdInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; @@ -765,7 +766,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { @Override public void onPackageUninstalled(@NonNull String packageName, int appId, @NonNull PackageState packageState, @Nullable AndroidPackage pkg, - @NonNull List<AndroidPackage> sharedUserPkgs, @UserIdInt int userId) { + @NonNull List<AndroidPackage> sharedUserPkgs, @CanBeALL @UserIdInt int userId) { if (userId != UserHandle.USER_ALL) { final int[] userIds = getAllUserIds(); if (!ArrayUtils.contains(userIds, userId)) { diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index e51ec04e60fe..33d57d5cc2b9 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -65,6 +65,7 @@ import android.annotation.AppIdInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; @@ -5284,7 +5285,7 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt @Override public void onPackageUninstalled(@NonNull String packageName, int appId, @NonNull PackageState packageState, @Nullable AndroidPackage pkg, - @NonNull List<AndroidPackage> sharedUserPkgs, @UserIdInt int userId) { + @NonNull List<AndroidPackage> sharedUserPkgs, @CanBeALL @UserIdInt int userId) { Objects.requireNonNull(packageState, "packageState"); Objects.requireNonNull(packageName, "packageName"); Objects.requireNonNull(sharedUserPkgs, "sharedUserPkgs"); diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java index 3d295f773805..f2491d949e6b 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java @@ -19,6 +19,7 @@ package com.android.server.pm.permission; import android.annotation.AppIdInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.PackageManager; @@ -662,5 +663,5 @@ public interface PermissionManagerServiceInterface extends PermissionManagerInte */ void onPackageUninstalled(@NonNull String packageName, int appId, @NonNull PackageState packageState, @Nullable AndroidPackage pkg, - @NonNull List<AndroidPackage> sharedUserPkgs, @UserIdInt int userId); + @NonNull List<AndroidPackage> sharedUserPkgs, @CanBeALL @UserIdInt int userId); } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java index a5c12840a645..ad765c8a0d54 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java @@ -18,6 +18,7 @@ package com.android.server.pm.permission; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeALL; import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.content.pm.PackageInstaller.SessionParams; @@ -325,7 +326,7 @@ public interface PermissionManagerServiceInternal extends PermissionManagerInter //@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) void onPackageUninstalled(@NonNull String packageName, int appId, @Nullable PackageState packageState, @Nullable AndroidPackage pkg, - @NonNull List<AndroidPackage> sharedUserPkgs, @UserIdInt int userId); + @NonNull List<AndroidPackage> sharedUserPkgs, @CanBeALL @UserIdInt int userId); /** * The permission-related parameters passed in for package installation. diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java index 44d787f790cf..c31c287017c3 100644 --- a/services/core/java/com/android/server/policy/PermissionPolicyService.java +++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java @@ -82,12 +82,10 @@ import android.util.LongSparseLongArray; import android.util.Slog; import android.util.SparseBooleanArray; -import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsService; import com.android.internal.infra.AndroidFuture; -import com.android.internal.policy.AttributeCache; import com.android.internal.util.IntPair; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.FgThread; @@ -167,6 +165,7 @@ public final class PermissionPolicyService extends SystemService { private Context mContext; private PackageManagerInternal mPackageManagerInternal; private PermissionManagerServiceInternal mPermissionManagerInternal; + private ActivityTaskManagerInternal mActivityTaskManagerInternal; private NotificationManagerInternal mNotificationManager; private TelephonyManager mTelephonyManager; private final KeyguardManager mKeyguardManager; @@ -189,6 +188,7 @@ public final class PermissionPolicyService extends SystemService { PackageManagerInternal.class); mPermissionManagerInternal = LocalServices.getService( PermissionManagerServiceInternal.class); + mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); final IAppOpsService appOpsService = IAppOpsService.Stub.asInterface( ServiceManager.getService(Context.APP_OPS_SERVICE)); @@ -1154,7 +1154,7 @@ public final class PermissionPolicyService extends SystemService { activityInfo.packageName, info.getCallingPackage(), info.getIntent(), info.getCheckedOptions(), activityInfo.name, true) - || isNoDisplayActivity(activityInfo)) { + || isNoDisplayActivity(activityInfo, info.getUserId())) { return; } UserHandle user = UserHandle.of(taskInfo.userId); @@ -1170,9 +1170,7 @@ public final class PermissionPolicyService extends SystemService { }; private void onActivityManagerReady() { - ActivityTaskManagerInternal atm = - LocalServices.getService(ActivityTaskManagerInternal.class); - atm.registerActivityStartInterceptor( + mActivityTaskManagerInternal.registerActivityStartInterceptor( ActivityInterceptorCallback.PERMISSION_POLICY_ORDERED_ID, mActivityInterceptorCallback); } @@ -1227,20 +1225,14 @@ public final class PermissionPolicyService extends SystemService { null, activityName, false); } - private boolean isNoDisplayActivity(@NonNull ActivityInfo aInfo) { + private boolean isNoDisplayActivity(@NonNull ActivityInfo aInfo, int userId) { final int themeResource = aInfo.getThemeResource(); if (themeResource == Resources.ID_NULL) { return false; } - boolean noDisplay = false; - final AttributeCache.Entry ent = AttributeCache.instance() - .get(aInfo.packageName, themeResource, R.styleable.Window, 0); - if (ent != null) { - noDisplay = ent.array.getBoolean(R.styleable.Window_windowNoDisplay, false); - } - - return noDisplay; + return mActivityTaskManagerInternal.isNoDisplay(aInfo.packageName, themeResource, + userId); } /** diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 76c5240ab623..980fb155999e 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -1163,6 +1163,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + private boolean shouldShowHub() { + final boolean hubEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), Settings.Secure.GLANCEABLE_HUB_ENABLED, + 1, mCurrentUserId) == 1; + + return mUserManagerInternal != null && mUserManagerInternal.isUserUnlocked(mCurrentUserId) + && hubEnabled && mDreamManagerInternal.dreamConditionActive(); + } + @VisibleForTesting void powerPress(long eventTime, int count, int displayId) { // SideFPS still needs to know about suppressed power buttons, in case it needs to block @@ -1251,7 +1260,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { mContext.getContentResolver(), Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, mCurrentUserId) == 1; - if (mDreamManagerInternal.isDreaming() || isKeyguardShowing()) { + if ((mDreamManagerInternal != null && mDreamManagerInternal.isDreaming()) + || isKeyguardShowing()) { // If the device is already dreaming or on keyguard, go to sleep. sleepDefaultDisplayFromPowerButton(eventTime, 0); break; @@ -1261,9 +1271,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { // show hub. boolean keyguardAvailable = !mLockPatternUtils.isLockScreenDisabled( mCurrentUserId); - if (mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled - && keyguardAvailable && mDreamManagerInternal.dreamConditionActive()) { - // If the hub can be launched, send a message to keyguard. + if (shouldShowHub() && keyguardAvailable) { + // If the hub can be launched, send a message to keyguard. We do not know if + // the hub is already running or not, keyguard handles turning screen off if + // it is. Bundle options = new Bundle(); options.putBoolean(EXTRA_TRIGGER_HUB, true); lockNow(options); @@ -1324,14 +1335,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { * @param isScreenOn Whether the screen is currently on. * @param noDreamAction The action to perform if dreaming is not possible. */ - private void attemptToDreamFromShortPowerButtonPress( + private boolean attemptToDreamFromShortPowerButtonPress( boolean isScreenOn, Runnable noDreamAction) { if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) { // If the power button behavior isn't one that should be able to trigger the dream, give // up. noDreamAction.run(); - return; + return false; } final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal(); @@ -1339,7 +1350,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { Slog.d(TAG, "Can't start dreaming when attempting to dream from short power" + " press (isScreenOn=" + isScreenOn + ")"); noDreamAction.run(); - return; + return false; } synchronized (mLock) { @@ -1350,6 +1361,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } dreamManagerInternal.requestDream(); + + return true; } /** @@ -2327,6 +2340,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { WindowWakeUpPolicy getWindowWakeUpPolicy() { return new WindowWakeUpPolicy(mContext); } + + DreamManagerInternal getDreamManagerInternal() { + return LocalServices.getService(DreamManagerInternal.class); + } } /** {@inheritDoc} */ @@ -2345,7 +2362,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); mInputManager = mContext.getSystemService(InputManager.class); mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); - mDreamManagerInternal = LocalServices.getService(DreamManagerInternal.class); + mDreamManagerInternal = injector.getDreamManagerInternal(); mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); mAppOpsManager = mContext.getSystemService(AppOpsManager.class); mSensorPrivacyManager = mContext.getSystemService(SensorPrivacyManager.class); @@ -3701,15 +3718,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; case KeyEvent.KEYCODE_N: if (firstDown && event.isMetaPressed()) { - if (event.isCtrlPressed()) { - sendSystemKeyToStatusBarAsync(event); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES); - } else { - toggleNotificationPanel(); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL); - } + toggleNotificationPanel(); + notifyKeyGestureCompleted(event, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL); return true; } break; @@ -6398,6 +6409,17 @@ public class PhoneWindowManager implements WindowManagerPolicy { event.getDisplayId(), event.getKeyCode(), "wakeUpFromWakeKey")) { return; } + + if (!shouldShowHub() + && mShortPressOnPowerBehavior == SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP + && event.getKeyCode() == KEYCODE_POWER + && attemptToDreamFromShortPowerButtonPress(false, () -> {})) { + // In the case that we should wake to dream and successfully initiate dreaming, do not + // continue waking up. Doing so will exit the dream state and cause UI to react + // accordingly. + return; + } + wakeUpFromWakeKey( event.getEventTime(), event.getKeyCode(), diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 1d62087428d8..2cf6b7efcb48 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -2911,19 +2911,7 @@ public class BatteryStatsImpl extends BatteryStats { return false; } - mCounter.getCounts(counts, procState); - - // Return counts only if at least one of the elements is non-zero. - for (int i = counts.length - 1; i >= 0; --i) { - if (counts[i] != 0) { - return true; - } - } - return false; - } - - public void logState(Printer pw, String prefix) { - pw.println(prefix + "mCounter=" + mCounter); + return mCounter.getCounts(counts, procState); } /** diff --git a/services/core/java/com/android/server/power/stats/format/ScreenPowerStatsLayout.java b/services/core/java/com/android/server/power/stats/format/ScreenPowerStatsLayout.java index 6f6a7ff5064a..ca2237562fe1 100644 --- a/services/core/java/com/android/server/power/stats/format/ScreenPowerStatsLayout.java +++ b/services/core/java/com/android/server/power/stats/format/ScreenPowerStatsLayout.java @@ -29,7 +29,8 @@ import com.android.internal.os.PowerStats; public class ScreenPowerStatsLayout extends PowerStatsLayout { private static final String EXTRA_DEVICE_SCREEN_COUNT = "dsc"; private static final String EXTRA_DEVICE_SCREEN_ON_DURATION_POSITION = "dsd"; - private static final String EXTRA_DEVICE_BRIGHTNESS_DURATION_POSITIONS = "dbd"; + private static final String EXTRA_DEVICE_BRIGHTNESS_DURATION_POSITIONS = "dbds"; + private static final String EXTRA_DEVICE_BRIGHTNESS_DURATION_POSITIONS_COMPAT = "dbd"; private static final String EXTRA_DEVICE_DOZE_DURATION_POSITION = "ddd"; private static final String EXTRA_DEVICE_DOZE_POWER_POSITION = "ddp"; private static final String EXTRA_UID_FOREGROUND_DURATION = "uf"; @@ -55,8 +56,14 @@ public class ScreenPowerStatsLayout extends PowerStatsLayout { PersistableBundle extras = descriptor.extras; mDisplayCount = extras.getInt(EXTRA_DEVICE_SCREEN_COUNT, 1); mDeviceScreenOnDurationPosition = extras.getInt(EXTRA_DEVICE_SCREEN_ON_DURATION_POSITION); - mDeviceBrightnessDurationPositions = extras.getIntArray( + mDeviceBrightnessDurationPositions = getIntArray(extras, EXTRA_DEVICE_BRIGHTNESS_DURATION_POSITIONS); + if (mDeviceBrightnessDurationPositions == null) { + // We should never put arrays in PowerStats.Descriptor because of the performance of + // .equals + mDeviceBrightnessDurationPositions = extras.getIntArray( + EXTRA_DEVICE_BRIGHTNESS_DURATION_POSITIONS_COMPAT); + } mDeviceScreenDozeDurationPosition = extras.getInt(EXTRA_DEVICE_DOZE_DURATION_POSITION); mDeviceScreenDozePowerPosition = extras.getInt(EXTRA_DEVICE_DOZE_POWER_POSITION); mUidTopActivityTimePosition = extras.getInt(EXTRA_UID_FOREGROUND_DURATION); @@ -67,7 +74,7 @@ public class ScreenPowerStatsLayout extends PowerStatsLayout { super.toExtras(extras); extras.putInt(EXTRA_DEVICE_SCREEN_COUNT, mDisplayCount); extras.putInt(EXTRA_DEVICE_SCREEN_ON_DURATION_POSITION, mDeviceScreenOnDurationPosition); - extras.putIntArray(EXTRA_DEVICE_BRIGHTNESS_DURATION_POSITIONS, + putIntArray(extras, EXTRA_DEVICE_BRIGHTNESS_DURATION_POSITIONS, mDeviceBrightnessDurationPositions); extras.putInt(EXTRA_DEVICE_DOZE_DURATION_POSITION, mDeviceScreenDozeDurationPosition); extras.putInt(EXTRA_DEVICE_DOZE_POWER_POSITION, mDeviceScreenDozePowerPosition); diff --git a/services/core/java/com/android/server/power/stats/format/SensorPowerStatsLayout.java b/services/core/java/com/android/server/power/stats/format/SensorPowerStatsLayout.java index e8df3ddfe5e8..c382534ac433 100644 --- a/services/core/java/com/android/server/power/stats/format/SensorPowerStatsLayout.java +++ b/services/core/java/com/android/server/power/stats/format/SensorPowerStatsLayout.java @@ -27,8 +27,10 @@ import java.util.Map; public class SensorPowerStatsLayout extends PowerStatsLayout { private static final String TAG = "SensorPowerStatsLayout"; - private static final String EXTRA_DEVICE_SENSOR_HANDLES = "dsh"; + private static final String EXTRA_DEVICE_SENSOR_HANDLES = "dshs"; + private static final String EXTRA_DEVICE_SENSOR_HANDLES_COMPAT = "dsh"; private static final String EXTRA_UID_SENSOR_POSITIONS = "usp"; + private static final String EXTRA_UID_SENSOR_POSITIONS_COMPAT = "usps"; private final SparseIntArray mSensorPositions = new SparseIntArray(); @@ -47,8 +49,14 @@ public class SensorPowerStatsLayout extends PowerStatsLayout { super(descriptor); PersistableBundle extras = descriptor.extras; - int[] handlers = extras.getIntArray(EXTRA_DEVICE_SENSOR_HANDLES); - int[] uidDurationPositions = extras.getIntArray(EXTRA_UID_SENSOR_POSITIONS); + int[] handlers = getIntArray(extras, EXTRA_DEVICE_SENSOR_HANDLES); + if (handlers == null) { + handlers = extras.getIntArray(EXTRA_DEVICE_SENSOR_HANDLES_COMPAT); + } + int[] uidDurationPositions = getIntArray(extras, EXTRA_UID_SENSOR_POSITIONS); + if (uidDurationPositions == null) { + uidDurationPositions = extras.getIntArray(EXTRA_UID_SENSOR_POSITIONS_COMPAT); + } if (handlers != null && uidDurationPositions != null) { for (int i = 0; i < handlers.length; i++) { @@ -69,8 +77,8 @@ public class SensorPowerStatsLayout extends PowerStatsLayout { uidDurationPositions[i] = mSensorPositions.valueAt(i); } - extras.putIntArray(EXTRA_DEVICE_SENSOR_HANDLES, handlers); - extras.putIntArray(EXTRA_UID_SENSOR_POSITIONS, uidDurationPositions); + putIntArray(extras, EXTRA_DEVICE_SENSOR_HANDLES, handlers); + putIntArray(extras, EXTRA_UID_SENSOR_POSITIONS, uidDurationPositions); } private void addUidSensorSection(int handle, String label) { diff --git a/services/core/java/com/android/server/power/stats/processor/AggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/processor/AggregatedPowerStats.java index a783d543559f..53894a196d24 100644 --- a/services/core/java/com/android/server/power/stats/processor/AggregatedPowerStats.java +++ b/services/core/java/com/android/server/power/stats/processor/AggregatedPowerStats.java @@ -25,6 +25,7 @@ import android.os.PersistableBundle; import android.os.UserHandle; import android.text.format.DateFormat; import android.util.IndentingPrintWriter; +import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -347,7 +348,10 @@ class AggregatedPowerStats { Set<Integer> uids = new HashSet<>(); for (int i = 0; i < mPowerComponentStats.size(); i++) { - mPowerComponentStats.valueAt(i).collectUids(uids); + IntArray activeUids = mPowerComponentStats.valueAt(i).getActiveUids(); + for (int j = activeUids.size() - 1; j >= 0; j--) { + uids.add(activeUids.get(j)); + } } Integer[] allUids = uids.toArray(new Integer[uids.size()]); diff --git a/services/core/java/com/android/server/power/stats/processor/AmbientDisplayPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/AmbientDisplayPowerStatsProcessor.java index 5f93bdf07f47..a550f2d820a9 100644 --- a/services/core/java/com/android/server/power/stats/processor/AmbientDisplayPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/AmbientDisplayPowerStatsProcessor.java @@ -68,11 +68,15 @@ class AmbientDisplayPowerStatsProcessor extends PowerStatsProcessor { // processor. All that remains to be done is copy the estimates over. MultiStateStats.States.forEachTrackedStateCombination(deviceStateConfig, states -> { - screenStats.getDeviceStats(mTmpScreenStats, states); + if (!screenStats.getDeviceStats(mTmpScreenStats, states)) { + return; + } double power = mScreenPowerStatsLayout.getScreenDozePowerEstimate(mTmpScreenStats); - mStatsLayout.setDevicePowerEstimate(mTmpDeviceStats, power); - stats.setDeviceStats(states, mTmpDeviceStats); + if (power != 0) { + mStatsLayout.setDevicePowerEstimate(mTmpDeviceStats, power); + stats.setDeviceStats(states, mTmpDeviceStats); + } }); } } diff --git a/services/core/java/com/android/server/power/stats/processor/BasePowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/BasePowerStatsProcessor.java index d24ea83540cb..1d359335c6d8 100644 --- a/services/core/java/com/android/server/power/stats/processor/BasePowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/BasePowerStatsProcessor.java @@ -24,13 +24,12 @@ import static com.android.server.power.stats.processor.AggregatedPowerStatsConfi import android.os.BatteryConsumer; import android.os.PersistableBundle; +import android.util.IntArray; import com.android.internal.os.PowerStats; import com.android.server.power.stats.format.BasePowerStatsLayout; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.function.DoubleSupplier; class BasePowerStatsProcessor extends PowerStatsProcessor { @@ -125,11 +124,12 @@ class BasePowerStatsProcessor extends PowerStatsProcessor { mCumulativeDischargeUah = 0; mCumulativeDischargeDurationMs = 0; - List<Integer> uids = new ArrayList<>(); - stats.collectUids(uids); - - long durationMs = timestampMs - mStartTimestamp; - if (!uids.isEmpty()) { + // Note that we are calling `getUids` rather than `getActiveUids`, because this Processor + // deals with duration rather than power estimation, so it needs to process *all* known + // UIDs, not just the ones that contributed PowerStats + IntArray uids = stats.getUids(); + if (uids.size() != 0) { + long durationMs = timestampMs - mStartTimestamp; for (int i = uids.size() - 1; i >= 0; i--) { long[] uidStats = new long[sStatsLayout.getUidStatsArrayLength()]; sStatsLayout.setUidUsageDuration(uidStats, durationMs); diff --git a/services/core/java/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessor.java index 9fe7f3e7a542..c89dddf45609 100644 --- a/services/core/java/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessor.java @@ -20,6 +20,7 @@ import android.annotation.IntDef; import android.os.BatteryStats; import android.os.PersistableBundle; import android.os.Process; +import android.util.IntArray; import com.android.internal.os.PowerStats; import com.android.server.power.stats.UsageBasedPowerEstimator; @@ -27,7 +28,6 @@ import com.android.server.power.stats.format.BinaryStatePowerStatsLayout; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -190,13 +190,13 @@ abstract class BinaryStatePowerStatsProcessor extends PowerStatsProcessor { } computeDevicePowerEstimates(stats, mPlan, mEnergyConsumerSupported); - combineDevicePowerEstimates(stats); - List<Integer> uids = new ArrayList<>(); - stats.collectUids(uids); - - computeUidActivityTotals(stats, uids); - computeUidPowerEstimates(stats, uids); + IntArray uids = stats.getActiveUids(); + if (uids.size() != 0) { + combineDevicePowerEstimates(stats); + computeUidActivityTotals(stats, uids); + computeUidPowerEstimates(stats, uids); + } } protected void computeDevicePowerEstimates(PowerComponentAggregatedPowerStats stats, @@ -239,8 +239,7 @@ abstract class BinaryStatePowerStatsProcessor extends PowerStatsProcessor { } } - private void computeUidActivityTotals(PowerComponentAggregatedPowerStats stats, - List<Integer> uids) { + private void computeUidActivityTotals(PowerComponentAggregatedPowerStats stats, IntArray uids) { for (int i = mPlan.uidStateEstimates.size() - 1; i >= 0; i--) { UidStateEstimate uidStateEstimate = mPlan.uidStateEstimates.get(i); Intermediates intermediates = @@ -259,8 +258,7 @@ abstract class BinaryStatePowerStatsProcessor extends PowerStatsProcessor { } } - private void computeUidPowerEstimates(PowerComponentAggregatedPowerStats stats, - List<Integer> uids) { + private void computeUidPowerEstimates(PowerComponentAggregatedPowerStats stats, IntArray uids) { for (int i = mPlan.uidStateEstimates.size() - 1; i >= 0; i--) { UidStateEstimate uidStateEstimate = mPlan.uidStateEstimates.get(i); Intermediates intermediates = @@ -276,12 +274,13 @@ abstract class BinaryStatePowerStatsProcessor extends PowerStatsProcessor { int uid = uids.get(k); if (stats.getUidStats(mTmpUidStatsArray, uid, proportionalEstimate.stateValues)) { - double power = intermediates.power - * mStatsLayout.getUidUsageDuration(mTmpUidStatsArray) - / intermediates.duration; - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, proportionalEstimate.stateValues, - mTmpUidStatsArray); + long duration = mStatsLayout.getUidUsageDuration(mTmpUidStatsArray); + if (duration != 0) { + double power = intermediates.power * duration / intermediates.duration; + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, + mTmpUidStatsArray); + } } } } diff --git a/services/core/java/com/android/server/power/stats/processor/BluetoothPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/BluetoothPowerStatsProcessor.java index 4c1a0db02273..c1cd3acf1656 100644 --- a/services/core/java/com/android/server/power/stats/processor/BluetoothPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/BluetoothPowerStatsProcessor.java @@ -16,12 +16,13 @@ package com.android.server.power.stats.processor; +import android.util.IntArray; + import com.android.internal.os.PowerProfile; import com.android.internal.os.PowerStats; import com.android.server.power.stats.UsageBasedPowerEstimator; import com.android.server.power.stats.format.BluetoothPowerStatsLayout; -import java.util.ArrayList; import java.util.List; class BluetoothPowerStatsProcessor extends PowerStatsProcessor { @@ -118,18 +119,19 @@ class BluetoothPowerStatsProcessor extends PowerStatsProcessor { combineDeviceStateEstimates(); - ArrayList<Integer> uids = new ArrayList<>(); - stats.collectUids(uids); - if (!uids.isEmpty()) { - for (int uid : uids) { - for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) { - computeUidActivityTotals(stats, uid, mPlan.uidStateEstimates.get(i)); + IntArray uids = stats.getActiveUids(); + if (uids.size() != 0) { + for (int i = uids.size() - 1; i >= 0; i--) { + int uid = uids.get(i); + for (int j = 0; j < mPlan.uidStateEstimates.size(); j++) { + computeUidActivityTotals(stats, uid, mPlan.uidStateEstimates.get(j)); } } - for (int uid : uids) { - for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) { - computeUidPowerEstimates(stats, uid, mPlan.uidStateEstimates.get(i)); + for (int i = uids.size() - 1; i >= 0; i--) { + int uid = uids.get(i); + for (int j = 0; j < mPlan.uidStateEstimates.size(); j++) { + computeUidPowerEstimates(stats, uid, mPlan.uidStateEstimates.get(j)); } } } @@ -297,8 +299,10 @@ class BluetoothPowerStatsProcessor extends PowerStatsProcessor { / intermediates.txBytes; } } - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + if (power != 0) { + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + } } } } diff --git a/services/core/java/com/android/server/power/stats/processor/CpuPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/CpuPowerStatsProcessor.java index 17ceca6e3dc1..98280beced9f 100644 --- a/services/core/java/com/android/server/power/stats/processor/CpuPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/CpuPowerStatsProcessor.java @@ -19,6 +19,7 @@ package com.android.server.power.stats.processor; import android.annotation.Nullable; import android.os.BatteryConsumer; import android.util.ArraySet; +import android.util.IntArray; import android.util.Log; import com.android.internal.os.CpuScalingPolicies; @@ -27,7 +28,6 @@ import com.android.internal.os.PowerStats; import com.android.server.power.stats.format.CpuPowerStatsLayout; import com.android.server.power.stats.format.WakelockPowerStatsLayout; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; @@ -119,6 +119,8 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { mTmpUidStatsArray = new long[descriptor.uidStatsArrayLength]; mWakelockDescriptor = null; + + initEnergyConsumerToPowerBracketMaps(); } /** @@ -157,9 +159,6 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { if (mPlan == null) { mPlan = new PowerEstimationPlan(stats.getConfig()); - if (mStatsLayout.getEnergyConsumerCount() != 0) { - initEnergyConsumerToPowerBracketMaps(); - } } Intermediates intermediates = new Intermediates(); @@ -189,12 +188,12 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { estimatePowerByDeviceState(stats, intermediates, wakelockStats); combineDeviceStateEstimates(); - ArrayList<Integer> uids = new ArrayList<>(); - stats.collectUids(uids); - if (!uids.isEmpty()) { - for (int uid : uids) { - for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) { - estimateUidPowerConsumption(stats, uid, mPlan.uidStateEstimates.get(i), + IntArray uids = stats.getActiveUids(); + if (uids.size() != 0) { + for (int i = uids.size() - 1; i >= 0; i--) { + int uid = uids.get(i); + for (int j = 0; j < mPlan.uidStateEstimates.size(); j++) { + estimateUidPowerConsumption(stats, uid, mPlan.uidStateEstimates.get(j), wakelockStats); } } @@ -255,6 +254,10 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { */ private void initEnergyConsumerToPowerBracketMaps() { int energyConsumerCount = mStatsLayout.getEnergyConsumerCount(); + if (energyConsumerCount == 0) { + return; + } + int powerBracketCount = mStatsLayout.getCpuPowerBracketCount(); mEnergyConsumerToCombinedEnergyConsumerMap = new int[energyConsumerCount]; @@ -404,7 +407,10 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { deviceStatsIntermediates.timeByBracket = new long[powerBracketCount]; deviceStatsIntermediates.powerByBracket = new double[powerBracketCount]; - stats.getDeviceStats(mTmpDeviceStatsArray, deviceStateEstimation.stateValues); + if (!stats.getDeviceStats(mTmpDeviceStatsArray, deviceStateEstimation.stateValues)) { + continue; + } + for (int step = 0; step < cpuScalingStepCount; step++) { if (intermediates.timeByScalingStep[step] == 0) { continue; @@ -429,16 +435,19 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { } if (wakelockStats != null) { - wakelockStats.getDeviceStats(mTmpWakelockDeviceStats, - deviceStateEstimation.stateValues); - double wakelockPowerEstimate = mWakelockPowerStatsLayout.getDevicePowerEstimate( - mTmpWakelockDeviceStats); - power = Math.max(0, power - wakelockPowerEstimate); + if (wakelockStats.getDeviceStats(mTmpWakelockDeviceStats, + deviceStateEstimation.stateValues)) { + double wakelockPowerEstimate = mWakelockPowerStatsLayout.getDevicePowerEstimate( + mTmpWakelockDeviceStats); + power = Math.max(0, power - wakelockPowerEstimate); + } } - deviceStatsIntermediates.power = power; - mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, power); - stats.setDeviceStats(deviceStateEstimation.stateValues, mTmpDeviceStatsArray); + if (power != 0) { + deviceStatsIntermediates.power = power; + mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, power); + stats.setDeviceStats(deviceStateEstimation.stateValues, mTmpDeviceStatsArray); + } } } @@ -538,15 +547,18 @@ class CpuPowerStatsProcessor extends PowerStatsProcessor { } if (wakelockStats != null) { - wakelockStats.getUidStats(mTmpWakelockUidStats, uid, - proportionalEstimate.stateValues); - double wakelockPowerEstimate = mWakelockPowerStatsLayout.getUidPowerEstimate( - mTmpWakelockUidStats); - power = Math.max(0, power - wakelockPowerEstimate); + if (wakelockStats.getUidStats(mTmpWakelockUidStats, uid, + proportionalEstimate.stateValues)) { + double wakelockPowerEstimate = mWakelockPowerStatsLayout.getUidPowerEstimate( + mTmpWakelockUidStats); + power = Math.max(0, power - wakelockPowerEstimate); + } } - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + if (power != 0) { + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + } } } } diff --git a/services/core/java/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsProcessor.java index 76adc47cc165..76ea7e841106 100644 --- a/services/core/java/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsProcessor.java @@ -16,10 +16,11 @@ package com.android.server.power.stats.processor; +import android.util.IntArray; + import com.android.internal.os.PowerStats; import com.android.server.power.stats.format.EnergyConsumerPowerStatsLayout; -import java.util.ArrayList; import java.util.List; class CustomEnergyConsumerPowerStatsProcessor extends PowerStatsProcessor { @@ -40,10 +41,8 @@ class CustomEnergyConsumerPowerStatsProcessor extends PowerStatsProcessor { computeDevicePowerEstimates(stats); - List<Integer> uids = new ArrayList<>(); - stats.collectUids(uids); - - if (!uids.isEmpty()) { + IntArray uids = stats.getActiveUids(); + if (uids.size() != 0) { computeUidPowerEstimates(stats, uids); } } @@ -62,7 +61,7 @@ class CustomEnergyConsumerPowerStatsProcessor extends PowerStatsProcessor { } private void computeUidPowerEstimates(PowerComponentAggregatedPowerStats stats, - List<Integer> uids) { + IntArray uids) { for (int i = mPlan.uidStateEstimates.size() - 1; i >= 0; i--) { UidStateEstimate uidStateEstimate = mPlan.uidStateEstimates.get(i); List<UidStateProportionalEstimate> proportionalEstimates = @@ -73,9 +72,12 @@ class CustomEnergyConsumerPowerStatsProcessor extends PowerStatsProcessor { int uid = uids.get(k); if (stats.getUidStats(mTmpUidStatsArray, uid, proportionalEstimate.stateValues)) { - sLayout.setUidPowerEstimate(mTmpUidStatsArray, - uCtoMah(sLayout.getUidConsumedEnergy(mTmpUidStatsArray, 0))); - stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + double power = uCtoMah(sLayout.getUidConsumedEnergy(mTmpUidStatsArray, 0)); + if (power != 0) { + sLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, + mTmpUidStatsArray); + } } } } diff --git a/services/core/java/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessor.java index b4c40de862b4..5883505326ad 100644 --- a/services/core/java/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessor.java @@ -19,6 +19,7 @@ import android.os.BatteryStats; import android.telephony.CellSignalStrength; import android.telephony.ModemActivityInfo; import android.telephony.ServiceState; +import android.util.IntArray; import android.util.Log; import android.util.Slog; import android.util.SparseArray; @@ -29,7 +30,6 @@ import com.android.internal.power.ModemPowerProfile; import com.android.server.power.stats.UsageBasedPowerEstimator; import com.android.server.power.stats.format.MobileRadioPowerStatsLayout; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -198,18 +198,19 @@ class MobileRadioPowerStatsProcessor extends PowerStatsProcessor { combineDeviceStateEstimates(); - ArrayList<Integer> uids = new ArrayList<>(); - stats.collectUids(uids); - if (!uids.isEmpty()) { - for (int uid : uids) { - for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) { - computeUidRxTxTotals(stats, uid, mPlan.uidStateEstimates.get(i)); + IntArray uids = stats.getActiveUids(); + if (uids.size() != 0) { + for (int i = uids.size() - 1; i >= 0; i--) { + int uid = uids.get(i); + for (int j = 0; j < mPlan.uidStateEstimates.size(); j++) { + computeUidRxTxTotals(stats, uid, mPlan.uidStateEstimates.get(j)); } } - for (int uid : uids) { - for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) { - computeUidPowerEstimates(stats, uid, mPlan.uidStateEstimates.get(i)); + for (int i = uids.size() - 1; i >= 0; i--) { + int uid = uids.get(i); + for (int j = 0; j < mPlan.uidStateEstimates.size(); j++) { + computeUidPowerEstimates(stats, uid, mPlan.uidStateEstimates.get(j)); } } } @@ -258,7 +259,9 @@ class MobileRadioPowerStatsProcessor extends PowerStatsProcessor { stats.forEachStateStatsKey(key -> { RxTxPowerEstimators estimators = mRxTxPowerEstimators.get(key); - stats.getStateStats(mTmpStateStatsArray, key, deviceStates); + if (!stats.getStateStats(mTmpStateStatsArray, key, deviceStates)) { + return; + } long rxTime = mStatsLayout.getStateRxTime(mTmpStateStatsArray); intermediates.rxPower += estimators.mRxPowerEstimator.calculatePower(rxTime); for (int txLevel = 0; txLevel < ModemActivityInfo.getNumTxPowerLevels(); txLevel++) { @@ -382,8 +385,10 @@ class MobileRadioPowerStatsProcessor extends PowerStatsProcessor { / intermediates.txPackets; } - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + if (power != 0) { + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + } if (DEBUG) { Slog.d(TAG, "UID: " + uid diff --git a/services/core/java/com/android/server/power/stats/processor/MultiStateStats.java b/services/core/java/com/android/server/power/stats/processor/MultiStateStats.java index 28474a554b38..0038943d7cf8 100644 --- a/services/core/java/com/android/server/power/stats/processor/MultiStateStats.java +++ b/services/core/java/com/android/server/power/stats/processor/MultiStateStats.java @@ -16,6 +16,8 @@ package com.android.server.power.stats.processor; +import android.annotation.CheckResult; +import android.annotation.Nullable; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -333,18 +335,20 @@ class MultiStateStats { /** * Adds the delta to the metrics. The number of values must correspond to the dimension count - * supplied to the Factory constructor + * supplied to the Factory constructor. Null values is equivalent to an array of zeros. */ - void increment(long[] values, long timestampMs) { + void increment(@Nullable long[] values, long timestampMs) { mCounter.incrementValues(values, timestampMs); mTracking = true; } /** - * Returns accumulated stats for the specified composite state. + * Returns accumulated stats for the specified composite state or false if the results are + * all zeros. */ - void getStats(long[] outValues, int[] states) { - mCounter.getCounts(outValues, mFactory.getSerialState(states)); + @CheckResult + boolean getStats(long[] outValues, int[] states) { + return mCounter.getCounts(outValues, mFactory.getSerialState(states)); } /** @@ -388,15 +392,7 @@ class MultiStateStats { private void writeXmlForStates(TypedXmlSerializer serializer, int[] states, long[] values) throws IOException { - mCounter.getCounts(values, mFactory.getSerialState(states)); - boolean nonZero = false; - for (long value : values) { - if (value != 0) { - nonZero = true; - break; - } - } - if (!nonZero) { + if (!mCounter.getCounts(values, mFactory.getSerialState(states))) { return; } @@ -469,15 +465,7 @@ class MultiStateStats { StringBuilder sb = new StringBuilder(); long[] values = new long[mCounter.getArrayLength()]; States.forEachTrackedStateCombination(mFactory.mStates, states -> { - mCounter.getCounts(values, mFactory.getSerialState(states)); - boolean nonZero = false; - for (long value : values) { - if (value != 0) { - nonZero = true; - break; - } - } - if (!nonZero) { + if (!mCounter.getCounts(values, mFactory.getSerialState(states))) { return; } diff --git a/services/core/java/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessor.java index 3957ae0862dc..ad628e4632e1 100644 --- a/services/core/java/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessor.java @@ -69,12 +69,15 @@ class PhoneCallPowerStatsProcessor extends PowerStatsProcessor { // processor. All that remains to be done is copy the estimates over. MultiStateStats.States.forEachTrackedStateCombination(deviceStateConfig, states -> { - mobileRadioStats.getDeviceStats(mTmpMobileRadioDeviceStats, states); - double callPowerEstimate = - mMobileRadioStatsLayout.getDeviceCallPowerEstimate( - mTmpMobileRadioDeviceStats); - mStatsLayout.setDevicePowerEstimate(mTmpDeviceStats, callPowerEstimate); - stats.setDeviceStats(states, mTmpDeviceStats); + if (!mobileRadioStats.getDeviceStats(mTmpMobileRadioDeviceStats, states)) { + return; + } + double callPowerEstimate = mMobileRadioStatsLayout.getDeviceCallPowerEstimate( + mTmpMobileRadioDeviceStats); + if (callPowerEstimate != 0) { + mStatsLayout.setDevicePowerEstimate(mTmpDeviceStats, callPowerEstimate); + stats.setDeviceStats(states, mTmpDeviceStats); + } }); } } diff --git a/services/core/java/com/android/server/power/stats/processor/PowerComponentAggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/processor/PowerComponentAggregatedPowerStats.java index d4f8fd92fc6c..d285a5987710 100644 --- a/services/core/java/com/android/server/power/stats/processor/PowerComponentAggregatedPowerStats.java +++ b/services/core/java/com/android/server/power/stats/processor/PowerComponentAggregatedPowerStats.java @@ -16,11 +16,13 @@ package com.android.server.power.stats.processor; +import android.annotation.CheckResult; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.BatteryStats; import android.os.UserHandle; import android.util.IndentingPrintWriter; +import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; @@ -34,7 +36,6 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.StringWriter; import java.util.Arrays; -import java.util.Collection; import java.util.function.IntConsumer; /** @@ -72,11 +73,11 @@ class PowerComponentAggregatedPowerStats { private MultiStateStats mDeviceStats; private final SparseArray<MultiStateStats> mStateStats = new SparseArray<>(); private final SparseArray<UidStats> mUidStats = new SparseArray<>(); - private long[] mZeroArray; private static class UidStats { public int[] states; public MultiStateStats stats; + public boolean hasPowerStats; public boolean updated; } @@ -200,6 +201,7 @@ class PowerComponentAggregatedPowerStats { if (uidStats.stats == null) { createUidStats(uidStats, mPowerStatsTimestamp); } + uidStats.hasPowerStats = true; uidStats.stats.setStats(states, values); } @@ -240,6 +242,7 @@ class PowerComponentAggregatedPowerStats { } uidStats.stats.increment(powerStats.uidStats.valueAt(i), timestampMs); uidStats.updated = true; + uidStats.hasPowerStats = true; } // For UIDs not mentioned in the PowerStats object, we must assume a 0 increment. @@ -248,11 +251,8 @@ class PowerComponentAggregatedPowerStats { for (int i = mUidStats.size() - 1; i >= 0; i--) { PowerComponentAggregatedPowerStats.UidStats uidStats = mUidStats.valueAt(i); if (!uidStats.updated && uidStats.stats != null) { - if (mZeroArray == null - || mZeroArray.length != mPowerStatsDescriptor.uidStatsArrayLength) { - mZeroArray = new long[mPowerStatsDescriptor.uidStatsArrayLength]; - } - uidStats.stats.increment(mZeroArray, timestampMs); + // Null stands for an array of zeros + uidStats.stats.increment(null, timestampMs); } uidStats.updated = false; } @@ -267,6 +267,7 @@ class PowerComponentAggregatedPowerStats { mStateStats.clear(); for (int i = mUidStats.size() - 1; i >= 0; i--) { mUidStats.valueAt(i).stats = null; + mUidStats.valueAt(i).hasPowerStats = false; } } @@ -290,14 +291,33 @@ class PowerComponentAggregatedPowerStats { return uidStats; } - void collectUids(Collection<Integer> uids) { + IntArray getUids() { + IntArray uids = new IntArray(mUidStats.size()); + for (int i = mUidStats.size() - 1; i >= 0; i--) { + UidStats uidStats = mUidStats.valueAt(i); + if (uidStats.stats != null) { + uids.add(mUidStats.keyAt(i)); + } + } + return uids; + } + + IntArray getActiveUids() { + IntArray uids = new IntArray(mUidStats.size()); for (int i = mUidStats.size() - 1; i >= 0; i--) { - if (mUidStats.valueAt(i).stats != null) { + UidStats uidStats = mUidStats.valueAt(i); + if (uidStats.hasPowerStats) { uids.add(mUidStats.keyAt(i)); } } + return uids; } + /** + * Populates outValues with the stats for the specified states. If the stats are all 0, + * returns false, leaving outValues unchanged. + */ + @CheckResult boolean getDeviceStats(long[] outValues, int[] deviceStates) { if (deviceStates.length != mDeviceStateConfig.length) { throw new IllegalArgumentException( @@ -305,12 +325,16 @@ class PowerComponentAggregatedPowerStats { + " expected: " + mDeviceStateConfig.length); } if (mDeviceStats != null) { - mDeviceStats.getStats(outValues, deviceStates); - return true; + return mDeviceStats.getStats(outValues, deviceStates); } return false; } + /** + * Populates outValues with the stats for the specified key and device states. If the stats + * are all 0, returns false, leaving outValues unchanged. + */ + @CheckResult boolean getStateStats(long[] outValues, int key, int[] deviceStates) { if (deviceStates.length != mDeviceStateConfig.length) { throw new IllegalArgumentException( @@ -319,8 +343,7 @@ class PowerComponentAggregatedPowerStats { } MultiStateStats stateStats = mStateStats.get(key); if (stateStats != null) { - stateStats.getStats(outValues, deviceStates); - return true; + return stateStats.getStats(outValues, deviceStates); } return false; } @@ -331,6 +354,11 @@ class PowerComponentAggregatedPowerStats { } } + /** + * Populates outValues with the stats for the specified UID and UID states. If the stats are + * all 0, returns false, leaving outValues unchanged. + */ + @CheckResult boolean getUidStats(long[] outValues, int uid, int[] uidStates) { if (uidStates.length != mUidStateConfig.length) { throw new IllegalArgumentException( @@ -339,8 +367,7 @@ class PowerComponentAggregatedPowerStats { } UidStats uidStats = mUidStats.get(uid); if (uidStats != null && uidStats.stats != null) { - uidStats.stats.getStats(outValues, uidStates); - return true; + return uidStats.stats.getStats(outValues, uidStates); } return false; } @@ -516,6 +543,7 @@ class PowerComponentAggregatedPowerStats { if (uidStats.stats == null) { createUidStats(uidStats, UNKNOWN); } + uidStats.hasPowerStats = true; if (!uidStats.stats.readFromXml(parser)) { return false; } @@ -563,15 +591,7 @@ class PowerComponentAggregatedPowerStats { long[] values = new long[stats.getDimensionCount()]; MultiStateStats.States[] stateInfo = stats.getStates(); MultiStateStats.States.forEachTrackedStateCombination(stateInfo, states -> { - stats.getStats(values, states); - boolean nonZero = false; - for (long value : values) { - if (value != 0) { - nonZero = true; - break; - } - } - if (!nonZero) { + if (!stats.getStats(values, states)) { return; } diff --git a/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java index 177d12988a27..634415ece806 100644 --- a/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java +++ b/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java @@ -21,6 +21,7 @@ import android.os.AggregateBatteryConsumer; import android.os.BatteryConsumer; import android.os.BatteryUsageStats; import android.os.UidBatteryConsumer; +import android.util.IntArray; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -31,7 +32,6 @@ import com.android.server.power.stats.PowerStatsStore; import com.android.server.power.stats.format.BasePowerStatsLayout; import com.android.server.power.stats.format.PowerStatsLayout; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; @@ -180,8 +180,7 @@ class PowerStatsExporter { } } if (layout.isUidPowerAttributionSupported()) { - populateBatteryConsumers(batteryUsageStatsBuilder, - powerComponentStats, layout); + populateBatteryConsumers(batteryUsageStatsBuilder, powerComponentStats, layout); } populateBatteryLevelInfo(batteryUsageStatsBuilder, batteryLevelInfo); @@ -258,6 +257,11 @@ class PowerStatsExporter { BatteryUsageStats.Builder batteryUsageStatsBuilder, PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout) { + IntArray uids = powerComponentStats.getUids(); + if (uids.size() == 0) { + return; + } + AggregatedPowerStatsConfig.PowerComponent powerComponent = powerComponentStats.getConfig(); PowerStats.Descriptor descriptor = powerComponentStats.getPowerStatsDescriptor(); long[] uidStats = new long[descriptor.uidStatsArrayLength]; @@ -273,8 +277,6 @@ class PowerStatsExporter { breakDownByProcState = false; } - ArrayList<Integer> uids = new ArrayList<>(); - powerComponentStats.collectUids(uids); for (int screenState = 0; screenState < BatteryConsumer.SCREEN_STATE_COUNT; screenState++) { if (batteryUsageStatsBuilder.isScreenStateDataNeeded()) { if (screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { @@ -303,7 +305,7 @@ class PowerStatsExporter { private void populateUidBatteryConsumers( BatteryUsageStats.Builder batteryUsageStatsBuilder, PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout, - List<Integer> uids, AggregatedPowerStatsConfig.PowerComponent powerComponent, + IntArray uids, AggregatedPowerStatsConfig.PowerComponent powerComponent, long[] uidStats, boolean breakDownByProcState, @BatteryConsumer.ScreenState int screenState, @BatteryConsumer.PowerState int powerState) { @@ -319,7 +321,8 @@ class PowerStatsExporter { long[] durationByProcState = new long[breakDownByProcState ? BatteryConsumer.PROCESS_STATE_COUNT : 1]; double powerAllApps = 0; - for (int uid : uids) { + for (int i = uids.size() - 1; i >= 0; i--) { + int uid = uids.get(i); UidBatteryConsumer.Builder builder = batteryUsageStatsBuilder.getOrCreateUidBatteryConsumerBuilder(uid); diff --git a/services/core/java/com/android/server/power/stats/processor/ScreenPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/ScreenPowerStatsProcessor.java index 8e7498f38fcb..9df3d7eea27b 100644 --- a/services/core/java/com/android/server/power/stats/processor/ScreenPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/ScreenPowerStatsProcessor.java @@ -27,6 +27,7 @@ import static com.android.server.power.stats.processor.AggregatedPowerStatsConfi import static com.android.server.power.stats.processor.AggregatedPowerStatsConfig.STATE_SCREEN; import android.os.BatteryStats; +import android.util.IntArray; import android.util.Slog; import com.android.internal.os.PowerProfile; @@ -34,7 +35,6 @@ import com.android.internal.os.PowerStats; import com.android.server.power.stats.UsageBasedPowerEstimator; import com.android.server.power.stats.format.ScreenPowerStatsLayout; -import java.util.ArrayList; import java.util.List; class ScreenPowerStatsProcessor extends PowerStatsProcessor { @@ -116,10 +116,8 @@ class ScreenPowerStatsProcessor extends PowerStatsProcessor { computeDevicePowerEstimates(stats); combineDeviceStateEstimates(); - List<Integer> uids = new ArrayList<>(); - stats.collectUids(uids); - - if (!uids.isEmpty()) { + IntArray uids = stats.getActiveUids(); + if (uids.size() != 0) { computeUidPowerEstimates(stats, uids); } mPlan.resetIntermediates(); @@ -197,7 +195,7 @@ class ScreenPowerStatsProcessor extends PowerStatsProcessor { } private void computeUidPowerEstimates(PowerComponentAggregatedPowerStats stats, - List<Integer> uids) { + IntArray uids) { int[] uidStateValues = new int[stats.getConfig().getUidStateConfig().length]; uidStateValues[STATE_SCREEN] = SCREEN_STATE_ON; uidStateValues[STATE_PROCESS_STATE] = PROCESS_STATE_UNSPECIFIED; @@ -232,9 +230,11 @@ class ScreenPowerStatsProcessor extends PowerStatsProcessor { int uid = uids.get(j); if (stats.getUidStats(mTmpUidStatsArray, uid, uidStateValues)) { long duration = mStatsLayout.getUidTopActivityDuration(mTmpUidStatsArray); - double power = intermediates.power * duration / totalTopActivityDuration; - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, uidStateValues, mTmpUidStatsArray); + if (duration != 0) { + double power = intermediates.power * duration / totalTopActivityDuration; + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, uidStateValues, mTmpUidStatsArray); + } } } } diff --git a/services/core/java/com/android/server/power/stats/processor/SensorPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/SensorPowerStatsProcessor.java index 0bb028bce5af..284e6a9fce89 100644 --- a/services/core/java/com/android/server/power/stats/processor/SensorPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/SensorPowerStatsProcessor.java @@ -21,6 +21,7 @@ import android.hardware.SensorManager; import android.os.BatteryConsumer; import android.os.BatteryStats; import android.os.PersistableBundle; +import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; @@ -28,7 +29,6 @@ import com.android.internal.os.PowerStats; import com.android.server.power.stats.format.PowerStatsLayout; import com.android.server.power.stats.format.SensorPowerStatsLayout; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Supplier; @@ -207,11 +207,11 @@ class SensorPowerStatsProcessor extends PowerStatsProcessor { mPlan = new PowerEstimationPlan(stats.getConfig()); } - List<Integer> uids = new ArrayList<>(); - stats.collectUids(uids); - - computeUidPowerEstimates(stats, uids); - computeDevicePowerEstimates(stats); + IntArray uids = stats.getActiveUids(); + if (uids.size() != 0) { + computeUidPowerEstimates(stats, uids); + computeDevicePowerEstimates(stats); + } mPlan.resetIntermediates(); } @@ -239,9 +239,7 @@ class SensorPowerStatsProcessor extends PowerStatsProcessor { mLastUpdateTimestamp = timestamp; } - private void computeUidPowerEstimates( - PowerComponentAggregatedPowerStats stats, - List<Integer> uids) { + private void computeUidPowerEstimates(PowerComponentAggregatedPowerStats stats, IntArray uids) { List<Sensor> sensorList = mSensorManager.getSensorList(Sensor.TYPE_ALL); int[] uidSensorDurationPositions = new int[sensorList.size()]; double[] sensorPower = new double[sensorList.size()]; @@ -292,8 +290,7 @@ class SensorPowerStatsProcessor extends PowerStatsProcessor { } } - private void computeDevicePowerEstimates( - PowerComponentAggregatedPowerStats stats) { + private void computeDevicePowerEstimates(PowerComponentAggregatedPowerStats stats) { for (int i = mPlan.combinedDeviceStateEstimations.size() - 1; i >= 0; i--) { CombinedDeviceStateEstimate estimation = mPlan.combinedDeviceStateEstimations.get(i); @@ -301,12 +298,16 @@ class SensorPowerStatsProcessor extends PowerStatsProcessor { continue; } - if (!stats.getDeviceStats(mTmpDeviceStatsArray, estimation.stateValues)) { + double power = ((Intermediates) estimation.intermediates).power; + if (power == 0) { continue; } - mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, - ((Intermediates) estimation.intermediates).power); + if (!stats.getDeviceStats(mTmpDeviceStatsArray, estimation.stateValues)) { + Arrays.fill(mTmpDeviceStatsArray, 0); + } + + mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, power); stats.setDeviceStats(estimation.stateValues, mTmpDeviceStatsArray); } } diff --git a/services/core/java/com/android/server/power/stats/processor/WifiPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/WifiPowerStatsProcessor.java index 0df01cf7e5d1..8cc0b6eb6150 100644 --- a/services/core/java/com/android/server/power/stats/processor/WifiPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/processor/WifiPowerStatsProcessor.java @@ -16,6 +16,7 @@ package com.android.server.power.stats.processor; +import android.util.IntArray; import android.util.Slog; import com.android.internal.os.PowerProfile; @@ -23,7 +24,6 @@ import com.android.internal.os.PowerStats; import com.android.server.power.stats.UsageBasedPowerEstimator; import com.android.server.power.stats.format.WifiPowerStatsLayout; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -148,18 +148,19 @@ class WifiPowerStatsProcessor extends PowerStatsProcessor { combineDeviceStateEstimates(); - ArrayList<Integer> uids = new ArrayList<>(); - stats.collectUids(uids); - if (!uids.isEmpty()) { - for (int uid : uids) { - for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) { - computeUidActivityTotals(stats, uid, mPlan.uidStateEstimates.get(i)); + IntArray uids = stats.getActiveUids(); + if (uids.size() != 0) { + for (int i = uids.size() - 1; i >= 0; i--) { + int uid = uids.get(i); + for (int j = 0; j < mPlan.uidStateEstimates.size(); j++) { + computeUidActivityTotals(stats, uid, mPlan.uidStateEstimates.get(j)); } } - for (int uid : uids) { - for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) { - computeUidPowerEstimates(stats, uid, mPlan.uidStateEstimates.get(i)); + for (int i = uids.size() - 1; i >= 0; i--) { + int uid = uids.get(i); + for (int j = 0; j < mPlan.uidStateEstimates.size(); j++) { + computeUidPowerEstimates(stats, uid, mPlan.uidStateEstimates.get(j)); } } } @@ -374,8 +375,10 @@ class WifiPowerStatsProcessor extends PowerStatsProcessor { / intermediates.batchedScanDuration; } } - mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); - stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + if (power != 0) { + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + } if (DEBUG) { Slog.d(TAG, "UID: " + uid diff --git a/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java index 9d60a576d9bc..0f8212e23c20 100644 --- a/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java +++ b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java @@ -136,4 +136,9 @@ public final class ImmutableVolumeInfo { public boolean isVisibleForWrite(int userId) { return mVolumeInfo.isVisibleForWrite(userId); } + + @Override + public String toString() { + return mVolumeInfo.toString(); + } } diff --git a/services/core/java/com/android/server/storage/StorageSessionController.java b/services/core/java/com/android/server/storage/StorageSessionController.java index 342b864c6473..281aeb68f224 100644 --- a/services/core/java/com/android/server/storage/StorageSessionController.java +++ b/services/core/java/com/android/server/storage/StorageSessionController.java @@ -156,14 +156,15 @@ public final class StorageSessionController { StorageUserConnection connection = null; synchronized (mLock) { connection = mConnections.get(connectionUserId); - if (connection != null) { - Slog.i(TAG, "Notifying volume state changed for session with id: " + sessionId); - connection.notifyVolumeStateChanged(sessionId, - vol.buildStorageVolume(mContext, vol.getMountUserId(), false)); - } else { - Slog.w(TAG, "No available storage user connection for userId : " - + connectionUserId); - } + } + + if (connection != null) { + Slog.i(TAG, "Notifying volume state changed for session with id: " + sessionId); + connection.notifyVolumeStateChanged(sessionId, + vol.buildStorageVolume(mContext, vol.getMountUserId(), false)); + } else { + Slog.w(TAG, "No available storage user connection for userId : " + + connectionUserId); } } @@ -225,16 +226,18 @@ public final class StorageSessionController { String sessionId = vol.getId(); int userId = getConnectionUserIdForVolume(vol); + StorageUserConnection connection = null; synchronized (mLock) { - StorageUserConnection connection = mConnections.get(userId); - if (connection != null) { - Slog.i(TAG, "Removed session for vol with id: " + sessionId); - connection.removeSession(sessionId); - return connection; - } else { - Slog.w(TAG, "Session already removed for vol with id: " + sessionId); - return null; - } + connection = mConnections.get(userId); + } + + if (connection != null) { + Slog.i(TAG, "Removed session for vol with id: " + sessionId); + connection.removeSession(sessionId); + return connection; + } else { + Slog.w(TAG, "Session already removed for vol with id: " + sessionId); + return null; } } diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java index 4124cfb4f092..94e52cd1033a 100644 --- a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java +++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java @@ -203,4 +203,9 @@ public class WatchedVolumeInfo extends WatchableImpl { public boolean isVisibleForWrite(int userId) { return mVolumeInfo.isVisibleForWrite(userId); } + + @Override + public String toString() { + return mVolumeInfo.toString(); + } }
\ No newline at end of file diff --git a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java index 07d9ad16aca5..da6478bd3395 100644 --- a/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/AbstractVibratorStep.java @@ -124,6 +124,8 @@ abstract class AbstractVibratorStep extends Step { Slog.d(VibrationThread.TAG, "Turning off vibrator " + getVibratorId()); } + // Make sure we ignore any pending callback from old vibration commands. + conductor.nextVibratorCallbackStepId(getVibratorId()); controller.off(); getVibration().stats.reportVibratorOff(); mPendingVibratorOffDeadline = 0; diff --git a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java index e495af59a2f9..b3eead109999 100644 --- a/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/ComposePrimitivesVibratorStep.java @@ -73,7 +73,8 @@ final class ComposePrimitivesVibratorStep extends AbstractComposedVibratorStep { PrimitiveSegment[] primitivesArray = primitives.toArray(new PrimitiveSegment[primitives.size()]); - long vibratorOnResult = controller.on(primitivesArray, getVibration().id); + int stepId = conductor.nextVibratorCallbackStepId(getVibratorId()); + long vibratorOnResult = controller.on(primitivesArray, getVibration().id, stepId); handleVibratorOnResult(vibratorOnResult); getVibration().stats.reportComposePrimitives(vibratorOnResult, primitivesArray); diff --git a/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java index bb8e6eed5707..7b41457b5016 100644 --- a/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java +++ b/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java @@ -72,7 +72,8 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep { + controller.getVibratorInfo().getId()); } PwlePoint[] pwlesArray = pwles.toArray(new PwlePoint[pwles.size()]); - long vibratorOnResult = controller.on(pwlesArray, getVibration().id); + int stepId = conductor.nextVibratorCallbackStepId(getVibratorId()); + long vibratorOnResult = controller.on(pwlesArray, getVibration().id, stepId); handleVibratorOnResult(vibratorOnResult); getVibration().stats.reportComposePwle(vibratorOnResult, pwlesArray); diff --git a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java index e8952fafaf77..d003251ef307 100644 --- a/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/ComposePwleVibratorStep.java @@ -71,7 +71,8 @@ final class ComposePwleVibratorStep extends AbstractComposedVibratorStep { + controller.getVibratorInfo().getId()); } RampSegment[] pwlesArray = pwles.toArray(new RampSegment[pwles.size()]); - long vibratorOnResult = controller.on(pwlesArray, getVibration().id); + int stepId = conductor.nextVibratorCallbackStepId(getVibratorId()); + long vibratorOnResult = controller.on(pwlesArray, getVibration().id, stepId); handleVibratorOnResult(vibratorOnResult); getVibration().stats.reportComposePwle(vibratorOnResult, pwlesArray); diff --git a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java index a92ac679b0f4..b2cc1f60ca1d 100644 --- a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java +++ b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java @@ -148,7 +148,7 @@ final class ExternalVibrationSession extends Vibration } @Override - public void notifyVibratorCallback(int vibratorId, long vibrationId) { + public void notifyVibratorCallback(int vibratorId, long vibrationId, long stepId) { // ignored, external control does not expect callbacks from the vibrator } diff --git a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java index 4b23216258af..88bb181781bf 100644 --- a/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/PerformPrebakedVibratorStep.java @@ -64,7 +64,8 @@ final class PerformPrebakedVibratorStep extends AbstractComposedVibratorStep { } VibrationEffect fallback = getVibration().getFallback(prebaked.getEffectId()); - long vibratorOnResult = controller.on(prebaked, getVibration().id); + int stepId = conductor.nextVibratorCallbackStepId(getVibratorId()); + long vibratorOnResult = controller.on(prebaked, getVibration().id, stepId); handleVibratorOnResult(vibratorOnResult); getVibration().stats.reportPerformEffect(vibratorOnResult, prebaked); diff --git a/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java index 407f3d996798..d4bcc36aee18 100644 --- a/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java @@ -49,7 +49,8 @@ final class PerformVendorEffectVibratorStep extends AbstractVibratorStep { public List<Step> play() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "PerformVendorEffectVibratorStep"); try { - long vibratorOnResult = controller.on(effect, getVibration().id); + int stepId = conductor.nextVibratorCallbackStepId(getVibratorId()); + long vibratorOnResult = controller.on(effect, getVibration().id, stepId); vibratorOnResult = Math.min(vibratorOnResult, VENDOR_EFFECT_MAX_DURATION_MS); handleVibratorOnResult(vibratorOnResult); getVibration().stats.reportPerformVendorEffect(vibratorOnResult); diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java index 8478e7743183..26b9595e60cd 100644 --- a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.os.SystemClock; import android.os.Trace; import android.os.VibrationEffect; +import android.os.vibrator.Flags; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; import android.util.Slog; @@ -49,6 +50,10 @@ final class SetAmplitudeVibratorStep extends AbstractComposedVibratorStep { @Override public boolean acceptVibratorCompleteCallback(int vibratorId) { + if (Flags.fixVibrationThreadCallbackHandling()) { + // TODO: remove this method once flag removed. + return super.acceptVibratorCompleteCallback(vibratorId); + } // Ensure the super method is called and will reset the off timeout and boolean flag. // This is true if the vibrator was ON and this callback has the same vibratorId. if (!super.acceptVibratorCompleteCallback(vibratorId)) { @@ -161,7 +166,8 @@ final class SetAmplitudeVibratorStep extends AbstractComposedVibratorStep { "Turning on vibrator " + controller.getVibratorInfo().getId() + " for " + duration + "ms"); } - long vibratorOnResult = controller.on(duration, getVibration().id); + int stepId = conductor.nextVibratorCallbackStepId(getVibratorId()); + long vibratorOnResult = controller.on(duration, getVibration().id, stepId); handleVibratorOnResult(vibratorOnResult); getVibration().stats.reportVibratorOn(vibratorOnResult); return vibratorOnResult; diff --git a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java index 628221b09d77..309eb8c3b099 100644 --- a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java +++ b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java @@ -137,13 +137,13 @@ final class SingleVibrationSession implements VibrationSession, IBinder.DeathRec } @Override - public void notifyVibratorCallback(int vibratorId, long vibrationId) { + public void notifyVibratorCallback(int vibratorId, long vibrationId, long stepId) { if (vibrationId != mVibration.id) { return; } synchronized (mLock) { if (mConductor != null) { - mConductor.notifyVibratorComplete(vibratorId); + mConductor.notifyVibratorComplete(vibratorId, stepId); } } } diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java index 1b6ce9dacfa9..bda3d442956b 100644 --- a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java +++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java @@ -176,14 +176,14 @@ final class VendorVibrationSession extends IVibrationSession.Stub @Override public void onCancel() { - Slog.d(TAG, "Cancellation signal received, cancelling vibration session..."); + Slog.d(TAG, "Session cancellation signal received, aborting vibration session..."); requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true, /* isVendorRequest= */ true); } @Override public void binderDied() { - Slog.d(TAG, "Binder died, cancelling vibration session..."); + Slog.d(TAG, "Session binder died, aborting vibration session..."); requestEndSession(Status.CANCELLED_BINDER_DIED, /* shouldAbort= */ true, /* isVendorRequest= */ false); } @@ -218,19 +218,21 @@ final class VendorVibrationSession extends IVibrationSession.Stub } @Override - public void notifyVibratorCallback(int vibratorId, long vibrationId) { - // Ignore it, the session vibration playback doesn't depend on HAL timings + public void notifyVibratorCallback(int vibratorId, long vibrationId, long stepId) { + Slog.d(TAG, "Vibration callback received for vibration " + vibrationId + " step " + stepId + + " on vibrator " + vibratorId + ", ignoring..."); } @Override public void notifySyncedVibratorsCallback(long vibrationId) { - // Ignore it, the session vibration playback doesn't depend on HAL timings + Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId + + ", ignoring..."); } @Override public void notifySessionCallback() { + Slog.d(TAG, "Session callback received, ending vibration session..."); synchronized (mLock) { - Slog.d(TAG, "Session callback received, ending vibration session..."); // If end was not requested then the HAL has cancelled the session. maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON, /* isVendorRequest= */ false); @@ -307,7 +309,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub } } if (isAlreadyEnded) { - // Session already ended, make sure we end it in the HAL. + Slog.d(TAG, "Session already ended after starting the HAL, aborting..."); mHandler.post(() -> mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true)); } } @@ -335,8 +337,8 @@ final class VendorVibrationSession extends IVibrationSession.Stub public boolean maybeSetVibrationConductor(VibrationStepConductor conductor) { synchronized (mLock) { if (mConductor != null) { - Slog.d(TAG, "Vibration session still dispatching previous vibration," - + " new vibration ignored"); + Slog.d(TAG, "Session still dispatching previous vibration, new vibration " + + conductor.getVibration().id + " ignored"); return false; } mConductor = conductor; @@ -345,19 +347,22 @@ final class VendorVibrationSession extends IVibrationSession.Stub } private void requestEndSession(Status status, boolean shouldAbort, boolean isVendorRequest) { + Slog.d(TAG, "Session end request received with status " + status); boolean shouldTriggerSessionHook = false; synchronized (mLock) { maybeSetEndRequestLocked(status, isVendorRequest); - if (isStarted()) { - // Always trigger session hook after it has started, in case new request aborts an - // already finishing session. Wait for HAL callback before actually ending here. + if (!isEnded() && isStarted()) { + // Trigger session hook even if it was already triggered, in case a second request + // is aborting the ongoing/ending session. This might cause it to end right away. + // Wait for HAL callback before setting the end status. shouldTriggerSessionHook = true; } else { - // Session did not start in the HAL, end it right away. + // Session not active in the HAL, set end status right away. maybeSetStatusToRequestedLocked(); } } if (shouldTriggerSessionHook) { + Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort); mHandler.post(() -> mManagerHooks.endSession(mSessionId, shouldAbort)); } } @@ -368,6 +373,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub // End already requested, keep first requested status and time. return; } + Slog.d(TAG, "Session end request accepted for status " + status); mEndStatusRequest = status; mEndedByVendor = isVendorRequest; mEndTime = System.currentTimeMillis(); @@ -400,6 +406,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub // No end status was requested, nothing to set. return; } + Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest); mStatus = mEndStatusRequest; // Run client callback in separate thread. final Status endStatus = mStatus; @@ -407,7 +414,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub try { mCallback.onFinished(toSessionStatus(endStatus)); } catch (RemoteException e) { - Slog.e(TAG, "Error notifying vendor session is finishing", e); + Slog.e(TAG, "Error notifying vendor session finished", e); } }); } diff --git a/services/core/java/com/android/server/vibrator/VibrationSession.java b/services/core/java/com/android/server/vibrator/VibrationSession.java index ae95a70e2a4f..23715e392580 100644 --- a/services/core/java/com/android/server/vibrator/VibrationSession.java +++ b/services/core/java/com/android/server/vibrator/VibrationSession.java @@ -106,7 +106,7 @@ interface VibrationSession { * complete (e.g. on(), perform(), compose()). This does not mean the vibration is complete, * since its playback might have one or more interactions with the vibrator hardware. */ - void notifyVibratorCallback(int vibratorId, long vibrationId); + void notifyVibratorCallback(int vibratorId, long vibrationId, long stepId); /** * Notify all synced vibrators have completed the last synchronized command during the playback diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java index 1e20debe156d..36e13224a476 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java +++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java @@ -30,6 +30,7 @@ import android.os.vibrator.VibrationEffectSegment; import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; import com.android.server.vibrator.VibrationSession.Status; @@ -93,6 +94,8 @@ final class VibrationStepConductor { private final Object mLock = new Object(); @GuardedBy("mLock") private final IntArray mSignalVibratorsComplete; + @GuardedBy("mLock") + private final SparseIntArray mSignalVibratorStepIds; @Nullable @GuardedBy("mLock") private Vibration.EndInfo mSignalCancel = null; @@ -121,6 +124,8 @@ final class VibrationStepConductor { this.vibratorManagerHooks = vibratorManagerHooks; this.mSignalVibratorsComplete = new IntArray(mDeviceAdapter.getAvailableVibratorIds().length); + this.mSignalVibratorStepIds = + new SparseIntArray(mDeviceAdapter.getAvailableVibratorIds().length); } @Nullable @@ -418,7 +423,7 @@ final class VibrationStepConductor { * <p>This is a lightweight method intended to be called directly via native callbacks. * The state update is recorded for processing on the main execution thread (VibrationThread). */ - public void notifyVibratorComplete(int vibratorId) { + public void notifyVibratorComplete(int vibratorId, long stepId) { // HAL callbacks may be triggered directly within HAL calls, so these notifications // could be on the VibrationThread as it calls the HAL, or some other executor later. // Therefore no thread assertion is made here. @@ -428,6 +433,14 @@ final class VibrationStepConductor { } synchronized (mLock) { + if (Flags.fixVibrationThreadCallbackHandling() + && mSignalVibratorStepIds.get(vibratorId) != stepId) { + if (DEBUG) { + Slog.d(TAG, "Vibrator " + vibratorId + " callback for step=" + stepId + + " ignored, current step=" + mSignalVibratorStepIds.get(vibratorId)); + } + return; + } mSignalVibratorsComplete.add(vibratorId); mLock.notify(); } @@ -645,6 +658,26 @@ final class VibrationStepConductor { } } + /** + * Updates and returns the next step id value to be used in vibrator commands. + * + * <p>This new step id will be kept by this conductor to filter out old callbacks that might be + * triggered too late by the HAL, preventing them from affecting the ongoing vibration playback. + */ + public int nextVibratorCallbackStepId(int vibratorId) { + if (!Flags.fixVibrationThreadCallbackHandling()) { + return 0; + } + if (Build.IS_DEBUGGABLE) { + expectIsVibrationThread(true); + } + synchronized (mLock) { + int stepId = mSignalVibratorStepIds.get(vibratorId) + 1; + mSignalVibratorStepIds.put(vibratorId, stepId); + return stepId; + } + } + private static CombinedVibration.Sequential toSequential(CombinedVibration effect) { if (effect instanceof CombinedVibration.Sequential) { return (CombinedVibration.Sequential) effect; diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java index acb31ceb4027..ab13b0e88d04 100644 --- a/services/core/java/com/android/server/vibrator/VibratorController.java +++ b/services/core/java/com/android/server/vibrator/VibratorController.java @@ -72,7 +72,7 @@ final class VibratorController { public interface OnVibrationCompleteListener { /** Callback triggered when an active vibration command is complete. */ - void onComplete(int vibratorId, long vibrationId); + void onComplete(int vibratorId, long vibrationId, long stepId); } /** Representation of the vibrator state based on the interactions through this controller. */ @@ -285,11 +285,11 @@ final class VibratorController { * @return The positive duration of the vibration started, if successful, zero if the vibrator * do not support the input or a negative number if the operation failed. */ - public long on(long milliseconds, long vibrationId) { + public long on(long milliseconds, long vibrationId, long stepId) { Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on"); try { synchronized (mLock) { - long duration = mNativeWrapper.on(milliseconds, vibrationId); + long duration = mNativeWrapper.on(milliseconds, vibrationId, stepId); if (duration > 0) { mCurrentAmplitude = -1; updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); @@ -310,7 +310,7 @@ final class VibratorController { * @return The positive duration of the vibration started, if successful, zero if the vibrator * do not support the input or a negative number if the operation failed. */ - public long on(VibrationEffect.VendorEffect vendorEffect, long vibrationId) { + public long on(VibrationEffect.VendorEffect vendorEffect, long vibrationId, long stepId) { Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (vendor)"); synchronized (mLock) { Parcel vendorData = Parcel.obtain(); @@ -319,7 +319,7 @@ final class VibratorController { vendorData.setDataPosition(0); long duration = mNativeWrapper.performVendorEffect(vendorData, vendorEffect.getEffectStrength(), vendorEffect.getScale(), - vendorEffect.getAdaptiveScale(), vibrationId); + vendorEffect.getAdaptiveScale(), vibrationId, stepId); if (duration > 0) { mCurrentAmplitude = -1; updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); @@ -341,12 +341,12 @@ final class VibratorController { * @return The positive duration of the vibration started, if successful, zero if the vibrator * do not support the input or a negative number if the operation failed. */ - public long on(PrebakedSegment prebaked, long vibrationId) { + public long on(PrebakedSegment prebaked, long vibrationId, long stepId) { Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (Prebaked)"); try { synchronized (mLock) { long duration = mNativeWrapper.perform(prebaked.getEffectId(), - prebaked.getEffectStrength(), vibrationId); + prebaked.getEffectStrength(), vibrationId, stepId); if (duration > 0) { mCurrentAmplitude = -1; updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); @@ -367,14 +367,14 @@ final class VibratorController { * @return The positive duration of the vibration started, if successful, zero if the vibrator * do not support the input or a negative number if the operation failed. */ - public long on(PrimitiveSegment[] primitives, long vibrationId) { + public long on(PrimitiveSegment[] primitives, long vibrationId, long stepId) { Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (Primitive)"); try { if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) { return 0; } synchronized (mLock) { - long duration = mNativeWrapper.compose(primitives, vibrationId); + long duration = mNativeWrapper.compose(primitives, vibrationId, stepId); if (duration > 0) { mCurrentAmplitude = -1; updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); @@ -394,7 +394,7 @@ final class VibratorController { * * @return The duration of the effect playing, or 0 if unsupported. */ - public long on(RampSegment[] primitives, long vibrationId) { + public long on(RampSegment[] primitives, long vibrationId, long stepId) { Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE)"); try { if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) { @@ -402,7 +402,8 @@ final class VibratorController { } synchronized (mLock) { int braking = mVibratorInfo.getDefaultBraking(); - long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId); + long duration = mNativeWrapper.composePwle( + primitives, braking, vibrationId, stepId); if (duration > 0) { mCurrentAmplitude = -1; updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); @@ -422,14 +423,14 @@ final class VibratorController { * * @return The duration of the effect playing, or 0 if unsupported. */ - public long on(PwlePoint[] pwlePoints, long vibrationId) { + public long on(PwlePoint[] pwlePoints, long vibrationId, long stepId) { Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE v2)"); try { if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2)) { return 0; } synchronized (mLock) { - long duration = mNativeWrapper.composePwleV2(pwlePoints, vibrationId); + long duration = mNativeWrapper.composePwleV2(pwlePoints, vibrationId, stepId); if (duration > 0) { mCurrentAmplitude = -1; updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); @@ -544,26 +545,27 @@ final class VibratorController { private static native boolean isAvailable(long nativePtr); - private static native long on(long nativePtr, long milliseconds, long vibrationId); + private static native long on(long nativePtr, long milliseconds, long vibrationId, + long stepId); private static native void off(long nativePtr); private static native void setAmplitude(long nativePtr, float amplitude); private static native long performEffect(long nativePtr, long effect, long strength, - long vibrationId); + long vibrationId, long stepId); private static native long performVendorEffect(long nativePtr, Parcel vendorData, - long strength, float scale, float adaptiveScale, long vibrationId); + long strength, float scale, float adaptiveScale, long vibrationId, long stepId); private static native long performComposedEffect(long nativePtr, PrimitiveSegment[] effect, - long vibrationId); + long vibrationId, long stepId); private static native long performPwleEffect(long nativePtr, RampSegment[] effect, - int braking, long vibrationId); + int braking, long vibrationId, long stepId); private static native long performPwleV2Effect(long nativePtr, PwlePoint[] effect, - long vibrationId); + long vibrationId, long stepId); private static native void setExternalControl(long nativePtr, boolean enabled); @@ -595,8 +597,8 @@ final class VibratorController { } /** Turns vibrator on for given time. */ - public long on(long milliseconds, long vibrationId) { - return on(mNativePtr, milliseconds, vibrationId); + public long on(long milliseconds, long vibrationId, long stepId) { + return on(mNativePtr, milliseconds, vibrationId, stepId); } /** Turns vibrator off. */ @@ -610,30 +612,31 @@ final class VibratorController { } /** Turns vibrator on to perform one of the supported effects. */ - public long perform(long effect, long strength, long vibrationId) { - return performEffect(mNativePtr, effect, strength, vibrationId); + public long perform(long effect, long strength, long vibrationId, long stepId) { + return performEffect(mNativePtr, effect, strength, vibrationId, stepId); } /** Turns vibrator on to perform a vendor-specific effect. */ public long performVendorEffect(Parcel vendorData, long strength, float scale, - float adaptiveScale, long vibrationId) { + float adaptiveScale, long vibrationId, long stepId) { return performVendorEffect(mNativePtr, vendorData, strength, scale, adaptiveScale, - vibrationId); + vibrationId, stepId); } /** Turns vibrator on to perform effect composed of give primitives effect. */ - public long compose(PrimitiveSegment[] primitives, long vibrationId) { - return performComposedEffect(mNativePtr, primitives, vibrationId); + public long compose(PrimitiveSegment[] primitives, long vibrationId, long stepId) { + return performComposedEffect(mNativePtr, primitives, vibrationId, stepId); } /** Turns vibrator on to perform PWLE effect composed of given primitives. */ - public long composePwle(RampSegment[] primitives, int braking, long vibrationId) { - return performPwleEffect(mNativePtr, primitives, braking, vibrationId); + public long composePwle(RampSegment[] primitives, int braking, long vibrationId, + long stepId) { + return performPwleEffect(mNativePtr, primitives, braking, vibrationId, stepId); } /** Turns vibrator on to perform PWLE effect composed of given points. */ - public long composePwleV2(PwlePoint[] pwlePoints, long vibrationId) { - return performPwleV2Effect(mNativePtr, pwlePoints, vibrationId); + public long composePwleV2(PwlePoint[] pwlePoints, long vibrationId, long stepId) { + return performPwleV2Effect(mNativePtr, pwlePoints, vibrationId, stepId); } /** Enabled the device vibrator to be controlled by another service. */ diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 9de46c878194..3f5fc338ee3b 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -61,6 +61,7 @@ import android.os.VibratorInfo; import android.os.vibrator.Flags; import android.os.vibrator.IVibrationSessionCallback; import android.os.vibrator.PrebakedSegment; +import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; import android.os.vibrator.VibratorInfoFactory; @@ -786,7 +787,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { synchronized (mLock) { if (DEBUG) { - Slog.d(TAG, "Starting session " + session.getSessionId()); + Slog.d(TAG, "Starting vendor session " + session.getSessionId()); } Status ignoreStatus = null; @@ -864,13 +865,16 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Session already ended, possibly cancelled by app cancellation signal. return session.getStatus(); } - int mode = startAppOpModeLocked(session.getCallerInfo()); + CallerInfo callerInfo = session.getCallerInfo(); + int mode = startAppOpModeLocked(callerInfo); switch (mode) { case AppOpsManager.MODE_ALLOWED: Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0); // Make sure mCurrentVibration is set while triggering the HAL. mCurrentSession = session; if (!session.linkToDeath()) { + // Shouldn't happen. The method call already logs. + finishAppOpModeLocked(callerInfo); mCurrentSession = null; return Status.IGNORED_ERROR_TOKEN; } @@ -878,14 +882,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Slog.e(TAG, "Error starting session " + sessionId + " on vibrators " + Arrays.toString(session.getVibratorIds())); session.unlinkToDeath(); + finishAppOpModeLocked(callerInfo); mCurrentSession = null; return Status.IGNORED_UNSUPPORTED; } session.notifyStart(); return null; case AppOpsManager.MODE_ERRORED: - Slog.w(TAG, "Start AppOpsManager operation errored for uid " - + session.getCallerInfo().uid); + Slog.w(TAG, "Start AppOpsManager operation errored for uid " + callerInfo.uid); return Status.IGNORED_ERROR_APP_OPS; default: return Status.IGNORED_APP_OPS; @@ -1081,11 +1085,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Nullable private Status startVibrationOnThreadLocked(SingleVibrationSession session) { if (DEBUG) { - Slog.d(TAG, "Starting vibration " + session.getVibration().id + " on thread"); + Slog.d(TAG, "Starting vibration " + session.getVibration().id + " on thread"); } VibrationStepConductor conductor = createVibrationStepConductor(session.getVibration()); session.setVibrationConductor(conductor); - int mode = startAppOpModeLocked(session.getCallerInfo()); + CallerInfo callerInfo = session.getCallerInfo(); + int mode = startAppOpModeLocked(callerInfo); switch (mode) { case AppOpsManager.MODE_ALLOWED: Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0); @@ -1093,19 +1098,21 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mCurrentSession = session; if (!mCurrentSession.linkToDeath()) { // Shouldn't happen. The method call already logs. + finishAppOpModeLocked(callerInfo); mCurrentSession = null; // Aborted. return Status.IGNORED_ERROR_TOKEN; } if (!mVibrationThread.runVibrationOnVibrationThread(conductor)) { // Shouldn't happen. The method call already logs. session.setVibrationConductor(null); // Rejected by thread, clear it in session. + mCurrentSession.unlinkToDeath(); + finishAppOpModeLocked(callerInfo); mCurrentSession = null; // Aborted. return Status.IGNORED_ERROR_SCHEDULING; } return null; case AppOpsManager.MODE_ERRORED: - Slog.w(TAG, "Start AppOpsManager operation errored for uid " - + session.getCallerInfo().uid); + Slog.w(TAG, "Start AppOpsManager operation errored for uid " + callerInfo.uid); return Status.IGNORED_ERROR_APP_OPS; default: return Status.IGNORED_APP_OPS; @@ -1286,14 +1293,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } - private void onVibrationComplete(int vibratorId, long vibrationId) { + private void onVibrationComplete(int vibratorId, long vibrationId, long stepId) { synchronized (mLock) { if (mCurrentSession != null) { if (DEBUG) { - Slog.d(TAG, "Vibration " + vibrationId + " on vibrator " + vibratorId - + " complete, notifying thread"); + Slog.d(TAG, "Vibration " + vibrationId + " step " + stepId + + " on vibrator " + vibratorId + " complete, notifying thread"); } - mCurrentSession.notifyVibratorCallback(vibratorId, vibrationId); + mCurrentSession.notifyVibratorCallback(vibratorId, vibrationId, stepId); } } } @@ -1494,6 +1501,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private int checkAppOpModeLocked(CallerInfo callerInfo) { int mode = mAppOps.checkAudioOpNoThrow(AppOpsManager.OP_VIBRATE, callerInfo.attrs.getAudioUsage(), callerInfo.uid, callerInfo.opPkg); + if (DEBUG) { + int opMode = mAppOps.checkOpNoThrow(AppOpsManager.OP_VIBRATE, callerInfo.uid, + callerInfo.opPkg); + Slog.d(TAG, "Check AppOp mode VIBRATE for uid " + callerInfo.uid + " and package " + + callerInfo.opPkg + " returned audio=" + AppOpsManager.MODE_NAMES[mode] + + ", op=" + AppOpsManager.MODE_NAMES[opMode]); + } int fixedMode = fixupAppOpModeLocked(mode, callerInfo.attrs); if (mode != fixedMode && fixedMode == AppOpsManager.MODE_ALLOWED) { // If we're just ignoring the vibration op then this is set by DND and we should ignore @@ -1507,9 +1521,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { /** Start an operation in {@link AppOpsManager}, if allowed. */ @GuardedBy("mLock") private int startAppOpModeLocked(CallerInfo callerInfo) { - return fixupAppOpModeLocked( - mAppOps.startOpNoThrow(AppOpsManager.OP_VIBRATE, callerInfo.uid, callerInfo.opPkg), - callerInfo.attrs); + int mode = mAppOps.startOpNoThrow(AppOpsManager.OP_VIBRATE, callerInfo.uid, + callerInfo.opPkg); + if (DEBUG) { + Slog.d(TAG, "Start AppOp mode VIBRATE for uid " + callerInfo.uid + " and package " + + callerInfo.opPkg + " returned " + AppOpsManager.MODE_NAMES[mode]); + } + return fixupAppOpModeLocked(mode, callerInfo.attrs); } /** @@ -1518,6 +1536,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { */ @GuardedBy("mLock") private void finishAppOpModeLocked(CallerInfo callerInfo) { + if (DEBUG) { + Slog.d(TAG, "Finish AppOp mode VIBRATE for uid " + callerInfo.uid + " and package " + + callerInfo.opPkg); + } mAppOps.finishOp(AppOpsManager.OP_VIBRATE, callerInfo.uid, callerInfo.opPkg); } @@ -2078,10 +2100,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override - public void onComplete(int vibratorId, long vibrationId) { + public void onComplete(int vibratorId, long vibrationId, long stepId) { VibratorManagerService service = mServiceRef.get(); if (service != null) { - service.onVibrationComplete(vibratorId, vibrationId); + service.onVibrationComplete(vibratorId, vibrationId, stepId); } } } @@ -2651,7 +2673,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { CombinedVibration.ParallelCombination combination = CombinedVibration.startParallel(); while ("-v".equals(getNextOption())) { - int vibratorId = Integer.parseInt(getNextArgRequired()); + int vibratorId = parseInt(getNextArgRequired(), "Expected vibrator id after -v"); combination.addVibrator(vibratorId, nextEffect()); } runVibrate(commonOptions, combination.combine()); @@ -2663,7 +2685,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { CombinedVibration.SequentialCombination combination = CombinedVibration.startSequential(); while ("-v".equals(getNextOption())) { - int vibratorId = Integer.parseInt(getNextArgRequired()); + int vibratorId = parseInt(getNextArgRequired(), "Expected vibrator id after -v"); combination.addNext(vibratorId, nextEffect()); } runVibrate(commonOptions, combination.combine()); @@ -2688,7 +2710,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private int runHapticFeedback() { CommonOptions commonOptions = new CommonOptions(); - int constant = Integer.parseInt(getNextArgRequired()); + int constant = parseInt(getNextArgRequired(), "Expected haptic feedback constant id"); IBinder deathBinder = commonOptions.background ? VibratorManagerService.this : mShellCallbacksToken; @@ -2736,12 +2758,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if ("-a".equals(nextOption)) { hasAmplitude = true; } else if ("-w".equals(nextOption)) { - delay = Integer.parseInt(getNextArgRequired()); + delay = parseInt(getNextArgRequired(), "Expected delay millis after -w"); } } - long duration = Long.parseLong(getNextArgRequired()); - int amplitude = hasAmplitude ? Integer.parseInt(getNextArgRequired()) + long duration = parseInt(getNextArgRequired(), "Expected one-shot duration millis"); + int amplitude = hasAmplitude + ? parseInt(getNextArgRequired(), "Expected one-shot amplitude") : VibrationEffect.DEFAULT_AMPLITUDE; composition.addOffDuration(Duration.ofMillis(delay)); composition.addEffect(VibrationEffect.createOneShot(duration, amplitude)); @@ -2816,8 +2839,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { while ((nextOption = getNextOption()) != null) { switch (nextOption) { case "-a" -> isAdvanced = true; - case "-i" -> initialSharpness = Float.parseFloat(getNextArgRequired()); - case "-r" -> repeat = Integer.parseInt(getNextArgRequired()); + case "-i" -> initialSharpness = parseFloat(getNextArgRequired(), + "Expected initial sharpness after -i"); + case "-r" -> repeat = parseInt(getNextArgRequired(), + "Expected repeat index after -r"); } } @@ -2843,8 +2868,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // nextArg is not a duration, finish reading. break; } - intensity = Float.parseFloat(getNextArgRequired()); - sharpness = Float.parseFloat(getNextArgRequired()); + intensity = parseFloat(getNextArgRequired(), "Expected envelope intensity"); + sharpness = parseFloat(getNextArgRequired(), "Expected envelope sharpness"); builder.addControlPoint(intensity, sharpness, duration); pos++; } @@ -2872,16 +2897,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { getNextArgRequired(); // consume "waveform" String nextOption; while ((nextOption = getNextOption()) != null) { - if ("-a".equals(nextOption)) { - hasAmplitudes = true; - } else if ("-r".equals(nextOption)) { - repeat = Integer.parseInt(getNextArgRequired()); - } else if ("-w".equals(nextOption)) { - delay = Integer.parseInt(getNextArgRequired()); - } else if ("-f".equals(nextOption)) { - hasFrequencies = true; - } else if ("-c".equals(nextOption)) { - isContinuous = true; + switch (nextOption) { + case "-a" -> hasAmplitudes = true; + case "-f" -> hasFrequencies = true; + case "-c" -> isContinuous = true; + case "-r" -> repeat = parseInt(getNextArgRequired(), + "Expected repeat index after -r"); + case "-w" -> delay = parseInt(getNextArgRequired(), + "Expected delay millis after -w"); } } List<Integer> durations = new ArrayList<>(); @@ -2899,14 +2922,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { break; } if (hasAmplitudes) { - amplitudes.add( - Float.parseFloat(getNextArgRequired()) / VibrationEffect.MAX_AMPLITUDE); + int amplitude = parseInt(getNextArgRequired(), "Expected waveform amplitude"); + amplitudes.add((float) amplitude / VibrationEffect.MAX_AMPLITUDE); } else { amplitudes.add(nextAmplitude); nextAmplitude = 1 - nextAmplitude; } if (hasFrequencies) { - frequencies.add(Float.parseFloat(getNextArgRequired())); + frequencies.add( + parseFloat(getNextArgRequired(), "Expected waveform frequency")); } } @@ -2965,27 +2989,37 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if ("-b".equals(nextOption)) { shouldFallback = true; } else if ("-w".equals(nextOption)) { - delay = Integer.parseInt(getNextArgRequired()); + delay = parseInt(getNextArgRequired(), "Expected delay millis after -w"); } } - int effectId = Integer.parseInt(getNextArgRequired()); + int effectId = parseInt(getNextArgRequired(), "Expected prebaked effect id"); composition.addOffDuration(Duration.ofMillis(delay)); composition.addEffect(VibrationEffect.get(effectId, shouldFallback)); } private void addPrimitivesToComposition(VibrationEffect.Composition composition) { getNextArgRequired(); // consume "primitives" - String nextArg; - while ((nextArg = peekNextArg()) != null) { + while (peekNextArg() != null) { int delay = 0; - if ("-w".equals(nextArg)) { - getNextArgRequired(); // consume "-w" - delay = Integer.parseInt(getNextArgRequired()); - nextArg = peekNextArg(); + float scale = 1f; + int delayType = PrimitiveSegment.DEFAULT_DELAY_TYPE; + + String nextOption; + while ((nextOption = getNextOption()) != null) { + if ("-s".equals(nextOption)) { + scale = parseFloat(getNextArgRequired(), "Expected scale after -s"); + } else if ("-o".equals(nextOption)) { + delayType = VibrationEffect.Composition.DELAY_TYPE_RELATIVE_START_OFFSET; + delay = parseInt(getNextArgRequired(), "Expected offset millis after -o"); + } else if ("-w".equals(nextOption)) { + delayType = PrimitiveSegment.DEFAULT_DELAY_TYPE; + delay = parseInt(getNextArgRequired(), "Expected delay millis after -w"); + } } try { - composition.addPrimitive(Integer.parseInt(nextArg), /* scale= */ 1, delay); + String nextArg = peekNextArg(); // Just in case this is not a primitive. + composition.addPrimitive(Integer.parseInt(nextArg), scale, delay, delayType); getNextArgRequired(); // consume the primitive id } catch (NumberFormatException | NullPointerException e) { // nextArg is not describing a primitive, leave it to be consumed by outer loops @@ -3011,17 +3045,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { VibrationXmlParser.parseDocument(new StringReader(xml)); VibratorInfo combinedVibratorInfo = getCombinedVibratorInfo(); if (combinedVibratorInfo == null) { - throw new IllegalStateException( - "No combined vibrator info to parse vibration XML " + xml); + throw new IllegalStateException("No vibrator info available to parse XML"); } VibrationEffect effect = parsedVibration.resolve(combinedVibratorInfo); if (effect == null) { - throw new IllegalArgumentException( - "Parsed vibration cannot be resolved for vibration XML " + xml); + throw new IllegalArgumentException("Parsed XML cannot be resolved: " + xml); } return CombinedVibration.createParallel(effect); } catch (IOException e) { - throw new RuntimeException("Error parsing vibration XML " + xml, e); + throw new RuntimeException("Error parsing XML: " + xml, e); } } @@ -3039,16 +3071,30 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + private static int parseInt(String text, String errorMessage) { + try { + return Integer.parseInt(text); + } catch (NumberFormatException | NullPointerException e) { + throw new IllegalArgumentException(errorMessage, e); + } + } + + private static float parseFloat(String text, String errorMessage) { + try { + return Float.parseFloat(text); + } catch (NumberFormatException | NullPointerException e) { + throw new IllegalArgumentException(errorMessage, e); + } + } + @Override public void onHelp() { try (PrintWriter pw = getOutPrintWriter();) { pw.println("Vibrator Manager commands:"); pw.println(" help"); pw.println(" Prints this help text."); - pw.println(""); pw.println(" list"); - pw.println(" Prints the id of device vibrators. This does not include any "); - pw.println(" connected input device."); + pw.println(" Prints device vibrator ids; does not include input devices."); pw.println(" synced [options] <effect>..."); pw.println(" Vibrates effect on all vibrators in sync."); pw.println(" combined [options] (-v <vibrator-id> <effect>...)..."); @@ -3058,51 +3104,41 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println(" xml [options] <xml>"); pw.println(" Vibrates using combined vibration described in given XML string"); pw.println(" on all vibrators in sync. The XML could be:"); - pw.println(" XML containing a single effect, or"); - pw.println(" A vibration select XML containing multiple effects."); - pw.println(" Vibrates using combined vibration described in given XML string."); - pw.println(" XML containing a single effect it runs on all vibrators in sync."); + pw.println(" A single <vibration-effect>, or"); + pw.println(" A <vibration-select> containing multiple effects."); + pw.println(" feedback [options] <constant>"); + pw.println(" Performs a haptic feedback with the given constant."); pw.println(" cancel"); pw.println(" Cancels any active vibration"); - pw.println(" feedback [-f] [-d <description>] <constant>"); - pw.println(" Performs a haptic feedback with the given constant."); - pw.println(" The force (-f) option enables the `always` configuration, which"); - pw.println(" plays the haptic irrespective of the vibration intensity settings"); pw.println(""); pw.println("Effect commands:"); pw.println(" oneshot [-w delay] [-a] <duration> [<amplitude>]"); - pw.println(" Vibrates for duration milliseconds; ignored when device is on "); - pw.println(" DND (Do Not Disturb) mode; touch feedback strength user setting "); - pw.println(" will be used to scale amplitude."); + pw.println(" Vibrates for duration milliseconds."); pw.println(" If -w is provided, the effect will be played after the specified"); pw.println(" wait time in milliseconds."); pw.println(" If -a is provided, the command accepts a second argument for "); pw.println(" amplitude, in a scale of 1-255."); pw.print(" waveform [-w delay] [-r index] [-a] [-f] [-c] "); pw.println("(<duration> [<amplitude>] [<frequency>])..."); - pw.println(" Vibrates for durations and amplitudes in list; ignored when "); - pw.println(" device is on DND (Do Not Disturb) mode; touch feedback strength "); - pw.println(" user setting will be used to scale amplitude."); + pw.println(" Vibrates for durations and amplitudes in list."); pw.println(" If -w is provided, the effect will be played after the specified"); pw.println(" wait time in milliseconds."); pw.println(" If -r is provided, the waveform loops back to the specified"); - pw.println(" index (e.g. 0 loops from the beginning)"); + pw.println(" index (e.g. 0 loops from the beginning)."); pw.println(" If -a is provided, the command expects amplitude to follow each"); pw.println(" duration; otherwise, it accepts durations only and alternates"); - pw.println(" off/on"); + pw.println(" off/on."); pw.println(" If -f is provided, the command expects frequency to follow each"); - pw.println(" amplitude or duration; otherwise, it uses resonant frequency"); + pw.println(" amplitude or duration; otherwise, it uses resonant frequency."); pw.println(" If -c is provided, the waveform is continuous and will ramp"); pw.println(" between values; otherwise each entry is a fixed step."); pw.println(" Duration is in milliseconds; amplitude is a scale of 1-255;"); - pw.println(" frequency is an absolute value in hertz;"); + pw.println(" frequency is an absolute value in hertz."); pw.print(" envelope [-a] [-i initial sharpness] [-r index] "); pw.println("[<duration1> <intensity1> <sharpness1>]..."); pw.println(" Generates a vibration pattern based on a series of duration, "); pw.println(" intensity, and sharpness values. The total vibration time is "); - pw.println(" the sum of all durations; Ignored when device is on "); - pw.println(" DND (Do Not Disturb) mode; touch feedback strength user setting "); - pw.println(" will be used to scale amplitude."); + pw.println(" the sum of all durations."); pw.println(" If -a is provided, the waveform will use the advanced APIs to "); pw.println(" generate the vibration pattern and the input parameters "); pw.println(" become [<duration1> <amplitude1> <frequency1>]."); @@ -3111,19 +3147,20 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println(" If -r is provided, the waveform loops back to the specified index"); pw.println(" (e.g. 0 loops from the beginning)."); pw.println(" prebaked [-w delay] [-b] <effect-id>"); - pw.println(" Vibrates with prebaked effect; ignored when device is on DND "); - pw.println(" (Do Not Disturb) mode; touch feedback strength user setting "); - pw.println(" will be used to scale amplitude."); + pw.println(" Vibrates with prebaked effect."); pw.println(" If -w is provided, the effect will be played after the specified"); pw.println(" wait time in milliseconds."); pw.println(" If -b is provided, the prebaked fallback effect will be played if"); pw.println(" the device doesn't support the given effect-id."); - pw.println(" primitives ([-w delay] <primitive-id>)..."); - pw.println(" Vibrates with a composed effect; ignored when device is on DND "); - pw.println(" (Do Not Disturb) mode; touch feedback strength user setting "); - pw.println(" will be used to scale primitive intensities."); + pw.print(" primitives ([-w delay] [-o time] [-s scale]"); + pw.println("<primitive-id> [<scale>])..."); + pw.println(" Vibrates with a composed effect."); pw.println(" If -w is provided, the next primitive will be played after the "); pw.println(" specified wait time in milliseconds."); + pw.println(" If -o is provided, the next primitive will be played at the "); + pw.println(" specified start offset time in milliseconds."); + pw.println(" If -s is provided, the next primitive will be played with the"); + pw.println(" specified amplitude scale, in a scale of [0,1]."); pw.println(""); pw.println("Common Options:"); pw.println(" -f"); @@ -3134,6 +3171,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println(" -d <description>"); pw.println(" Add description to the vibration."); pw.println(""); + pw.println("Notes"); + pw.println(" Vibrations triggered by these commands will be ignored when"); + pw.println(" device is on DND (Do Not Disturb) mode; notification strength"); + pw.println(" user settings will be applied for scale."); + pw.println(""); } } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index ebadeac70dd1..7b6d408fbe2c 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -109,7 +109,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED; import static android.view.WindowManager.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING; -import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_UNSET; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.hasWindowExtensionsEnabled; @@ -321,9 +320,7 @@ import android.util.MergedConfiguration; import android.util.Slog; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; -import android.view.AppTransitionAnimationSpec; import android.view.DisplayInfo; -import android.view.IAppTransitionAnimationSpecsFuture; import android.view.InputApplicationHandle; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; @@ -493,6 +490,7 @@ final class ActivityRecord extends WindowToken { private long createTime = System.currentTimeMillis(); long lastVisibleTime; // last time this activity became visible long pauseTime; // last time we started pausing the activity + long mStoppedTime; // last time we completely stopped the activity long launchTickTime; // base time for launch tick messages long topResumedStateLossTime; // last time we reported top resumed state loss to an activity // Last configuration reported to the activity in the client process. @@ -2348,7 +2346,8 @@ final class ActivityRecord extends WindowToken { // The snapshot of home is only used once because it won't be updated while screen // is on (see {@link TaskSnapshotController#screenTurningOff}). mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId); - if ((mDisplayContent.mAppTransition.getTransitFlags() + final Transition transition = mTransitionController.getCollectingTransition(); + if (transition != null && (transition.getFlags() & WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) == 0) { // Only use snapshot of home as starting window when unlocking directly. return false; @@ -3637,7 +3636,6 @@ final class ActivityRecord extends WindowToken { if (DEBUG_VISIBILITY || DEBUG_TRANSITION) { Slog.v(TAG_TRANSITION, "Prepare close transition: finishing " + this); } - mDisplayContent.prepareAppTransition(TRANSIT_CLOSE); // When finishing the activity preemptively take the snapshot before the app window // is marked as hidden and any configuration changes take place @@ -3739,7 +3737,6 @@ final class ActivityRecord extends WindowToken { private void prepareActivityHideTransitionAnimation() { final DisplayContent dc = mDisplayContent; - dc.prepareAppTransition(TRANSIT_CLOSE); setVisibility(false); dc.executeAppTransition(); } @@ -4391,13 +4388,6 @@ final class ActivityRecord extends WindowToken { removeStartingWindow(); } - // If app transition animation was running for this activity, then we need to ensure that - // the app transition notifies that animations have completed in - // DisplayContent.handleAnimatingStoppedAndTransition(), so add to that list now - if (isAnimating(TRANSITION | PARENTS, ANIMATION_TYPE_APP_TRANSITION)) { - getDisplayContent().mNoAnimationNotifyOnTransitionFinished.add(token); - } - if (delayed && !isEmpty()) { // set the token aside because it has an active animation to be finished ProtoLog.v(WM_DEBUG_ADD_REMOVE, @@ -5069,8 +5059,6 @@ final class ActivityRecord extends WindowToken { void applyOptionsAnimation() { if (DEBUG_TRANSITION) Slog.i(TAG, "Applying options for " + this); if (mPendingRemoteAnimation != null) { - mDisplayContent.mAppTransition.overridePendingAppTransitionRemote( - mPendingRemoteAnimation); mTransitionController.setStatusBarTransitionDelay( mPendingRemoteAnimation.getStatusBarTransitionDelay()); } else { @@ -5100,14 +5088,6 @@ final class ActivityRecord extends WindowToken { IRemoteCallback finishCallback = null; switch (animationType) { case ANIM_CUSTOM: - displayContent.mAppTransition.overridePendingAppTransition( - pendingOptions.getPackageName(), - pendingOptions.getCustomEnterResId(), - pendingOptions.getCustomExitResId(), - pendingOptions.getCustomBackgroundColor(), - pendingOptions.getAnimationStartedListener(), - pendingOptions.getAnimationFinishedListener(), - pendingOptions.getOverrideTaskTransition()); options = AnimationOptions.makeCustomAnimOptions(pendingOptions.getPackageName(), pendingOptions.getCustomEnterResId(), pendingOptions.getCustomExitResId(), pendingOptions.getCustomBackgroundColor(), @@ -5116,9 +5096,6 @@ final class ActivityRecord extends WindowToken { finishCallback = pendingOptions.getAnimationFinishedListener(); break; case ANIM_CLIP_REVEAL: - displayContent.mAppTransition.overridePendingAppTransitionClipReveal( - pendingOptions.getStartX(), pendingOptions.getStartY(), - pendingOptions.getWidth(), pendingOptions.getHeight()); options = AnimationOptions.makeClipRevealAnimOptions( pendingOptions.getStartX(), pendingOptions.getStartY(), pendingOptions.getWidth(), pendingOptions.getHeight()); @@ -5130,9 +5107,6 @@ final class ActivityRecord extends WindowToken { } break; case ANIM_SCALE_UP: - displayContent.mAppTransition.overridePendingAppTransitionScaleUp( - pendingOptions.getStartX(), pendingOptions.getStartY(), - pendingOptions.getWidth(), pendingOptions.getHeight()); options = AnimationOptions.makeScaleUpAnimOptions( pendingOptions.getStartX(), pendingOptions.getStartY(), pendingOptions.getWidth(), pendingOptions.getHeight(), @@ -5148,10 +5122,6 @@ final class ActivityRecord extends WindowToken { case ANIM_THUMBNAIL_SCALE_DOWN: final boolean scaleUp = (animationType == ANIM_THUMBNAIL_SCALE_UP); final HardwareBuffer buffer = pendingOptions.getThumbnail(); - displayContent.mAppTransition.overridePendingAppTransitionThumb(buffer, - pendingOptions.getStartX(), pendingOptions.getStartY(), - pendingOptions.getAnimationStartedListener(), - scaleUp); options = AnimationOptions.makeThumbnailAnimOptions(buffer, pendingOptions.getStartX(), pendingOptions.getStartY(), scaleUp); startCallback = pendingOptions.getAnimationStartedListener(); @@ -5164,36 +5134,9 @@ final class ActivityRecord extends WindowToken { break; case ANIM_THUMBNAIL_ASPECT_SCALE_UP: case ANIM_THUMBNAIL_ASPECT_SCALE_DOWN: - final AppTransitionAnimationSpec[] specs = pendingOptions.getAnimSpecs(); - final IAppTransitionAnimationSpecsFuture specsFuture = - pendingOptions.getSpecsFuture(); - if (specsFuture != null) { - displayContent.mAppTransition.overridePendingAppTransitionMultiThumbFuture( - specsFuture, pendingOptions.getAnimationStartedListener(), - animationType == ANIM_THUMBNAIL_ASPECT_SCALE_UP); - } else if (animationType == ANIM_THUMBNAIL_ASPECT_SCALE_DOWN - && specs != null) { - displayContent.mAppTransition.overridePendingAppTransitionMultiThumb( - specs, pendingOptions.getAnimationStartedListener(), - pendingOptions.getAnimationFinishedListener(), false); - } else { - displayContent.mAppTransition.overridePendingAppTransitionAspectScaledThumb( - pendingOptions.getThumbnail(), - pendingOptions.getStartX(), pendingOptions.getStartY(), - pendingOptions.getWidth(), pendingOptions.getHeight(), - pendingOptions.getAnimationStartedListener(), - (animationType == ANIM_THUMBNAIL_ASPECT_SCALE_UP)); - if (intent.getSourceBounds() == null) { - intent.setSourceBounds(new Rect(pendingOptions.getStartX(), - pendingOptions.getStartY(), - pendingOptions.getStartX() + pendingOptions.getWidth(), - pendingOptions.getStartY() + pendingOptions.getHeight())); - } - } + // TODO(b/397847511): remove the related types from ActivityOptions. break; case ANIM_OPEN_CROSS_PROFILE_APPS: - displayContent.mAppTransition - .overridePendingAppTransitionStartCrossProfileApps(); options = AnimationOptions.makeCrossProfileAnimOptions(); break; case ANIM_NONE: @@ -5450,8 +5393,6 @@ final class ActivityRecord extends WindowToken { } private void setVisibility(boolean visible, boolean deferHidingClient) { - final AppTransition appTransition = getDisplayContent().mAppTransition; - // Don't set visibility to false if we were already not visible. This prevents WM from // adding the app to the closing app list which doesn't make sense for something that is // already not visible. However, set visibility to true even if we are already visible. @@ -5471,8 +5412,8 @@ final class ActivityRecord extends WindowToken { } ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, - "setAppVisibility(%s, visible=%b): %s visible=%b mVisibleRequested=%b Callers=%s", - token, visible, appTransition, isVisible(), mVisibleRequested, + "setAppVisibility(%s, visible=%b): visible=%b mVisibleRequested=%b Callers=%s", + token, visible, isVisible(), mVisibleRequested, Debug.getCallers(6)); // Before setting mVisibleRequested so we can track changes. @@ -5569,15 +5510,6 @@ final class ActivityRecord extends WindowToken { updateReportedVisibilityLocked(); } - @Override - boolean applyAnimation(LayoutParams lp, @TransitionOldType int transit, boolean enter, - boolean isVoiceInteraction, @Nullable ArrayList<WindowContainer> sources) { - if ((mTransitionChangeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { - return false; - } - return super.applyAnimation(lp, transit, enter, isVoiceInteraction, sources); - } - /** * Update visibility to this {@link ActivityRecord}. * @@ -6447,6 +6379,7 @@ final class ActivityRecord extends WindowToken { Slog.w(TAG, "Exception thrown during pause", e); // Just in case, assume it to be stopped. mAppStopped = true; + mStoppedTime = SystemClock.uptimeMillis(); ProtoLog.v(WM_DEBUG_STATES, "Stop failed; moving to STOPPED: %s", this); setState(STOPPED, "stopIfPossible"); } @@ -6480,6 +6413,7 @@ final class ActivityRecord extends WindowToken { if (isStopping) { ProtoLog.v(WM_DEBUG_STATES, "Moving to STOPPED: %s (stop complete)", this); + mStoppedTime = SystemClock.uptimeMillis(); setState(STOPPED, "activityStopped"); } @@ -6639,9 +6573,7 @@ final class ActivityRecord extends WindowToken { // starting window is drawn, the transition can start earlier. Exclude finishing and bubble // because it may be a trampoline. if (app == null && !finishing && !mLaunchedFromBubble - && mVisibleRequested && !mDisplayContent.mAppTransition.isReady() - && !mDisplayContent.mAppTransition.isRunning() - && mDisplayContent.isNextTransitionForward()) { + && mVisibleRequested && mDisplayContent.isNextTransitionForward()) { // The pending transition state will be cleared after the transition is started, so // save the state for launching the client later (used by LaunchActivityItem). mStartingData.mIsTransitionForward = true; @@ -7523,7 +7455,6 @@ final class ActivityRecord extends WindowToken { } } - getDisplayContent().mAppTransition.notifyAppTransitionFinishedLocked(token); scheduleAnimation(); // Schedule to handle the stopping and finishing activities which the animation is done @@ -9644,7 +9575,18 @@ final class ActivityRecord extends WindowToken { && !mDisplayContent.isSleeping()) { // Visibility of starting activities isn't calculated until pause-complete, so if // this is not paused yet, don't consider it ready. - return false; + // However, due to pip1 having an intermediate state, add a special exception here + // that skips waiting if the next activity is already visible. + final ActivityRecord toResume = isPip2ExperimentEnabled() ? null + : mDisplayContent.getActivity((r) -> !r.finishing + && r.isVisibleRequested() + && !r.isTaskOverlay() + && !r.isAlwaysOnTop()); + if (toResume == null || !toResume.isVisible()) { + return false; + } else { + Slog.i(TAG, "Assuming sync-finish while pausing due to visible target"); + } } return true; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 0a57cb50d681..c243cdc23137 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -617,6 +617,9 @@ public abstract class ActivityTaskManagerInternal { */ public abstract boolean isBaseOfLockedTask(String packageName); + /** Returns the value of {@link android.R.attr#windowNoDisplay} from the given theme. */ + public abstract boolean isNoDisplay(String packageName, int theme, int userId); + /** * Creates an interface to update configuration for the calling application. */ diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 4e2fade599ba..6f83822ee97a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -2130,6 +2130,26 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override + public boolean setTaskIsPerceptible(int taskId, boolean isPerceptible) { + enforceTaskPermission("setTaskIsPerceptible()"); + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + final Task task = mRootWindowContainer.anyTaskForId(taskId, + MATCH_ATTACHED_TASK_ONLY); + if (task == null) { + Slog.w(TAG, "setTaskIsPerceptible: No task to set with id=" + taskId); + return false; + } + task.mIsPerceptible = isPerceptible; + } + return true; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public boolean removeTask(int taskId) { mAmInternal.enforceCallingPermission(REMOVE_TASKS, "removeTask()"); synchronized (mGlobalLock) { @@ -7448,6 +7468,18 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override + public boolean isNoDisplay(String packageName, int theme, int userId) { + if (!com.android.window.flags.Flags.cacheWindowStyle()) { + final AttributeCache.Entry ent = AttributeCache.instance() + .get(packageName, theme, R.styleable.Window, userId); + return ent != null + && ent.array.getBoolean(R.styleable.Window_windowNoDisplay, false); + } + final ActivityRecord.WindowStyle style = getWindowStyle(packageName, theme, userId); + return style != null && style.noDisplay(); + } + + @Override public PackageConfigurationUpdater createPackageConfigurationUpdater() { return new PackageConfigurationUpdaterImpl(Binder.getCallingPid(), ActivityTaskManagerService.this); diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index b932ef362aca..6718ae435cd9 100644 --- a/services/core/java/com/android/server/wm/DisplayArea.java +++ b/services/core/java/com/android/server/wm/DisplayArea.java @@ -748,14 +748,9 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> { && policy.okToAnimate(true /* ignoreScreenOn */)) { return false; } - // Consider unoccluding only when all unknown visibilities have been - // resolved, as otherwise we just may be starting another occluding activity. - final boolean isUnoccluding = - mDisplayContent.mAppTransition.isUnoccluding() - && mDisplayContent.mUnknownAppVisibilityController.allResolved(); - // If keyguard is showing, or we're unoccluding, force the keyguard's orientation, + // Use keyguard's orientation if it is showing and not occluded // even if SystemUI hasn't updated the attrs yet. - if (policy.isKeyguardShowingAndNotOccluded() || isUnoccluding) { + if (policy.isKeyguardShowingAndNotOccluded()) { return true; } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index fd322a5c6345..214a3a15bb44 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -88,7 +88,6 @@ import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY; import static android.window.DisplayAreaOrganizer.FEATURE_IME; import static android.window.DisplayAreaOrganizer.FEATURE_ROOT; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_BOOT; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_CONTENT_RECORDING; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_FOCUS; @@ -162,6 +161,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.content.ComponentCallbacks; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; @@ -368,12 +368,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp private MetricsLogger mMetricsLogger; - /** - * List of clients without a transtiton animation that we notify once we are done - * transitioning since they won't be notified through the app window animator. - */ - final List<IBinder> mNoAnimationNotifyOnTransitionFinished = new ArrayList<>(); - // Mapping from a token IBinder to a WindowToken object on this display. private final HashMap<IBinder, WindowToken> mTokenMap = new HashMap(); @@ -456,6 +450,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp private DisplayInfo mLastDisplayInfoOverride; private final DisplayMetrics mDisplayMetrics = new DisplayMetrics(); + @NonNull private final DisplayPolicy mDisplayPolicy; private final DisplayRotation mDisplayRotation; @@ -542,6 +537,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** Remove this display when animation on it has completed. */ private boolean mDeferredRemoval; + @NonNull final PinnedTaskController mPinnedTaskController; private final LinkedList<ActivityRecord> mTmpUpdateAllDrawn = new LinkedList(); @@ -826,28 +822,36 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ private final ToBooleanFunction<WindowState> mFindFocusedWindow = w -> { final ActivityRecord focusedApp = mFocusedApp; + final boolean canReceiveKeys = w.canReceiveKeys(); ProtoLog.v(WM_DEBUG_FOCUS, "Looking for focus: %s, flags=%d, canReceive=%b, reason=%s", - w, w.mAttrs.flags, w.canReceiveKeys(), + w, w.mAttrs.flags, canReceiveKeys, w.canReceiveKeysReason(false /* fromUserTouch */)); - if (!w.canReceiveKeys()) { + if (!canReceiveKeys) { return false; } - // When switching the app task, we keep the IME window visibility for better - // transitioning experiences. - // However, in case IME created a child window or the IME selection dialog without - // dismissing during the task switching to keep the window focus because IME window has - // higher window hierarchy, we don't give it focus if the next IME layering target - // doesn't request IME visible. - if (w.mIsImWindow && w.isChildWindow() && (mImeLayeringTarget == null - || !mImeLayeringTarget.isRequestedVisible(ime()))) { - return false; - } - if (w.mAttrs.type == TYPE_INPUT_METHOD_DIALOG && mImeLayeringTarget != null - && !(mImeLayeringTarget.isRequestedVisible(ime()) - && mImeLayeringTarget.isVisibleRequested())) { - return false; + // IME windows remain visibleRequested while switching apps to maintain a smooth animation. + // This persists until the new app is focused, so they can be visibleRequested despite not + // being visible to the user (i.e. occluded). These rank higher in the window hierarchy than + // app windows, so they will always be considered first. To avoid having the focus stuck, + // an IME window (child or not) cannot be focused if the IME parent is not visible. However, + // child windows also require the IME to be visible in the current app. + if (w.mIsImWindow) { + final boolean imeParentVisible = mInputMethodSurfaceParentWindow != null + && mInputMethodSurfaceParentWindow.isVisibleRequested(); + if (!imeParentVisible) { + ProtoLog.v(WM_DEBUG_FOCUS, "findFocusedWindow: IME window not focusable as" + + " IME parent is not visible"); + return false; + } + + if (w.isChildWindow() + && !getInsetsStateController().getImeSourceProvider().isImeShowing()) { + ProtoLog.v(WM_DEBUG_FOCUS, "findFocusedWindow: IME child window not focusable as" + + " IME is not visible"); + return false; + } } final ActivityRecord activity = w.mActivityRecord; @@ -1102,6 +1106,29 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp }; /** + * Called to update fields retrieve from {@link #getDisplayUiContext()} resources when + * there's a configuration update on {@link #getDisplayUiContext()}. + */ + @NonNull + private final ComponentCallbacks mSysUiContextConfigCallback = new ComponentCallbacks() { + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + synchronized (mWmService.mGlobalLock) { + if (mDisplayReady) { + mDisplayPolicy.onConfigurationChanged(); + mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp(); + } + } + } + + @Override + public void onLowMemory() { + // Do nothing. + } + }; + + /** * Create new {@link DisplayContent} instance, add itself to the root window container and * initialize direct children. * @param display May not be null. @@ -1145,8 +1172,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mFixedRotationTransitionListener = new FixedRotationTransitionListener(mDisplayId); mAppTransition = new AppTransition(mWmService.mContext, mWmService, this); - mAppTransition.registerListenerLocked(mWmService.mActivityManagerAppTransitionNotifier); - mAppTransition.registerListenerLocked(mFixedRotationTransitionListener); mTransitionController.registerLegacyListener(mFixedRotationTransitionListener); mUnknownAppVisibilityController = new UnknownAppVisibilityController(mWmService, this); mRemoteDisplayChangeController = new RemoteDisplayChangeController(this); @@ -2797,11 +2822,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final int lastOrientation = getConfiguration().orientation; final int lastWindowingMode = getWindowingMode(); super.onConfigurationChanged(newParentConfig); - if (mDisplayPolicy != null) { - mDisplayPolicy.onConfigurationChanged(); - mPinnedTaskController.onPostDisplayConfigurationChanged(); - mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp(); + if (!Flags.trackSystemUiContextBeforeWms()) { + mSysUiContextConfigCallback.onConfigurationChanged(newParentConfig); } + mPinnedTaskController.onPostDisplayConfigurationChanged(); // Update IME parent if needed. updateImeParent(); @@ -2833,13 +2857,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return isVisible() && !mRemoved && !mRemoving; } - @Override - void onAppTransitionDone() { - super.onAppTransitionDone(); - mWmService.mWindowsChanged = true; - onTransitionFinished(); - } - void onTransitionFinished() { // If the transition finished callback cannot match the token for some reason, make sure the // rotated state is cleared if it is already invisible. @@ -3360,9 +3377,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDeferredRemoval = false; try { mUnknownAppVisibilityController.clear(); - mAppTransition.removeAppTransitionTimeoutCallbacks(); mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener); - handleAnimatingStoppedAndTransition(); mDeviceStateController.unregisterDeviceStateCallback(mDeviceStateConsumer); super.removeImmediately(); if (DEBUG_DISPLAY) Slog.v(TAG_WM, "Removing display=" + this); @@ -3381,6 +3396,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp .getKeyguardController().onDisplayRemoved(mDisplayId); mWallpaperController.resetLargestDisplay(mDisplay); mWmService.mDisplayWindowSettings.onDisplayRemoved(this); + if (Flags.trackSystemUiContextBeforeWms()) { + getDisplayUiContext().unregisterComponentCallbacks(mSysUiContextConfigCallback); + } } finally { mDisplayReady = false; } @@ -3542,11 +3560,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplayRotation.dumpDebug(proto, DISPLAY_ROTATION); mDisplayFrames.dumpDebug(proto, DISPLAY_FRAMES); proto.write(MIN_SIZE_OF_RESIZEABLE_TASK_DP, mMinSizeOfResizeableTaskDp); - if (mTransitionController.isShellTransitionsEnabled()) { - mTransitionController.dumpDebugLegacy(proto, APP_TRANSITION); - } else { - mAppTransition.dumpDebug(proto, APP_TRANSITION); - } + mTransitionController.dumpDebugLegacy(proto, APP_TRANSITION); if (mFocusedApp != null) { mFocusedApp.writeNameToProto(proto, FOCUSED_APP); } @@ -3817,7 +3831,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (mTmpWindow == null) { ProtoLog.v(WM_DEBUG_FOCUS_LIGHT, "findFocusedWindow: No focusable windows, display=%d", getDisplayId()); - return null; } return mTmpWindow; } @@ -5429,7 +5442,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp reconfigureDisplayLocked(); onRequestedOverrideConfigurationChanged(getRequestedOverrideConfiguration()); mWmService.mDisplayNotificationController.dispatchDisplayAdded(this); - // Attach the SystemUiContext to this DisplayContent the get latest configuration. + // Attach the SystemUiContext to this DisplayContent to get latest configuration. // Note that the SystemUiContext will be removed automatically if this DisplayContent // is detached. registerSystemUiContext(); @@ -5437,11 +5450,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } private void registerSystemUiContext() { + final Context systemUiContext = getDisplayUiContext(); final WindowProcessController wpc = mAtmService.getProcessController( - getDisplayUiContext().getIApplicationThread()); + systemUiContext.getIApplicationThread()); mWmService.mWindowContextListenerController.registerWindowContainerListener( - wpc, getDisplayUiContext().getWindowContextToken(), this, + wpc, systemUiContext.getWindowContextToken(), this, INVALID_WINDOW_TYPE, null /* options */); + if (Flags.trackSystemUiContextBeforeWms()) { + systemUiContext.registerComponentCallbacks(mSysUiContextConfigCallback); + } } @Override @@ -5603,61 +5620,20 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * @see AppTransition#prepareAppTransition */ void requestTransitionAndLegacyPrepare(@WindowManager.TransitionType int transit, - @WindowManager.TransitionFlags int flags) { - prepareAppTransition(transit, flags); - mTransitionController.requestTransitionIfNeeded(transit, flags, null /* trigger */, this); + @WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger) { + mTransitionController.requestTransitionIfNeeded(transit, flags, trigger, this); } void executeAppTransition() { mTransitionController.setReady(this); - if (mAppTransition.isTransitionSet()) { - ProtoLog.w(WM_DEBUG_APP_TRANSITIONS, - "Execute app transition: %s, displayId: %d Callers=%s", - mAppTransition, mDisplayId, Debug.getCallers(5)); - mAppTransition.setReady(); - mWmService.mWindowPlacerLocked.requestTraversal(); - } - } - - /** - * Update pendingLayoutChanges after app transition has finished. - */ - void handleAnimatingStoppedAndTransition() { - int changes = 0; - - mAppTransition.setIdle(); - - for (int i = mNoAnimationNotifyOnTransitionFinished.size() - 1; i >= 0; i--) { - final IBinder token = mNoAnimationNotifyOnTransitionFinished.get(i); - mAppTransition.notifyAppTransitionFinishedLocked(token); - } - mNoAnimationNotifyOnTransitionFinished.clear(); - - mWallpaperController.hideDeferredWallpapersIfNeededLegacy(); - - onAppTransitionDone(); - - changes |= FINISH_LAYOUT_REDO_LAYOUT; - ProtoLog.v(WM_DEBUG_WALLPAPER, "Wallpaper layer changed: assigning layers + relayout"); - computeImeTarget(true /* updateImeTarget */); - mWallpaperMayChange = true; - // Since the window list has been rebuilt, focus might have to be recomputed since the - // actual order of windows might have changed again. - mWmService.mFocusMayChange = true; - - pendingLayoutChanges |= changes; } /** Check if pending app transition is for activity / task launch. */ boolean isNextTransitionForward() { // TODO(b/191375840): decouple "forwardness" from transition system. - if (mTransitionController.isShellTransitionsEnabled()) { - @WindowManager.TransitionType int type = - mTransitionController.getCollectingTransitionType(); - return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT; - } - return mAppTransition.containsTransitRequest(TRANSIT_OPEN) - || mAppTransition.containsTransitRequest(TRANSIT_TO_FRONT); + final @WindowManager.TransitionType int type = + mTransitionController.getCollectingTransitionType(); + return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT; } /** @@ -6620,6 +6596,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp forAllTasks((t) -> { t.getRootTask().removeChild(t, "removeAllTasks"); }); } + @NonNull Context getDisplayUiContext() { return mDisplayPolicy.getSystemUiContext(); } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 10f591cfd379..7aa2101f516c 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -631,7 +631,6 @@ public class DisplayPolicy { mHandler.post(mAppTransitionFinished); } }; - displayContent.mAppTransition.registerListenerLocked(mAppTransitionListener); displayContent.mTransitionController.registerLegacyListener(mAppTransitionListener); // TODO: Make it can take screenshot on external display @@ -1865,6 +1864,7 @@ public class DisplayPolicy { return mContext; } + @NonNull Context getSystemUiContext() { return mUiContext; } diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 6091b8334438..6d73739e5046 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -250,7 +250,7 @@ class KeyguardController { // to the locked state before holding the sleep token again if (!ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS) { dc.requestTransitionAndLegacyPrepare( - TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING); + TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING, /* trigger= */ null); } dc.mWallpaperController.adjustWallpaperWindows(); dc.executeAppTransition(); diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java index 69463433827f..b3cff9c6cc3d 100644 --- a/services/core/java/com/android/server/wm/PresentationController.java +++ b/services/core/java/com/android/server/wm/PresentationController.java @@ -56,11 +56,6 @@ class PresentationController { ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s", win.getDisplayId(), win); mPresentingDisplayIds.add(win.getDisplayId()); - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, - /*notifyClients=*/ true); - } win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true); } @@ -76,11 +71,6 @@ class PresentationController { if (displayIdIndex != -1) { mPresentingDisplayIds.remove(displayIdIndex); } - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, - /*notifyClients=*/ true); - } win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false); } } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 40f16c187f20..f309372ab6a2 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2103,10 +2103,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> removeRootTasksInWindowingModes(WINDOWING_MODE_PINNED); } - // Set a transition to ensure that we don't immediately try and update the visibility - // of the activity entering PIP - r.getDisplayContent().prepareAppTransition(TRANSIT_NONE); - transitionController.collect(task); // Defer the windowing mode change until after the transition to prevent the activity diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 6b3499a5d68c..6cd1336122be 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -54,7 +54,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED; -import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -504,6 +503,17 @@ class Task extends TaskFragment { int mOffsetYForInsets; /** + * When set to true, the task will be kept at a PERCEPTIBLE_APP_ADJ, and downgraded + * to PREVIOUS_APP_ADJ if not in foreground for a period of time. + * One example use case is for desktop form factors, where it is important keep tasks in the + * perceptible state (rather than cached where it may be frozen) when a user moves it to the + * foreground. + * On startup, restored Tasks will not be perceptible, until user actually interacts with it + * (i.e. brings it to the foreground) + */ + boolean mIsPerceptible = false; + + /** * Whether the compatibility overrides that change the resizability of the app should be allowed * for the specific app. */ @@ -1647,8 +1657,7 @@ class Task extends TaskFragment { // Prevent the transition from being executed too early if the top activity is // resumed but the mVisibleRequested of any other activity is true, the transition // should wait until next activity resumed. - if (r.isState(RESUMED) || (r.isVisible() - && !mDisplayContent.mAppTransition.containsTransitRequest(TRANSIT_CLOSE))) { + if (r.isState(RESUMED) || r.isVisible()) { r.finishIfPossible(reason, false /* oomAdj */); } else { r.destroyIfPossible(reason); @@ -2324,11 +2333,6 @@ class Task extends TaskFragment { mLastSurfaceSize.set(width, height); } - @VisibleForTesting - boolean isInChangeTransition() { - return AppTransition.isChangeTransitOld(mTransit); - } - @Override void writeIdentifierToProto(ProtoOutputStream proto, long fieldId) { final long token = proto.start(fieldId); @@ -3854,6 +3858,7 @@ class Task extends TaskFragment { pw.print(ActivityInfo.resizeModeToString(mResizeMode)); pw.print(" mSupportsPictureInPicture="); pw.print(mSupportsPictureInPicture); pw.print(" isResizeable="); pw.println(isResizeable()); + pw.print(" isPerceptible="); pw.println(mIsPerceptible); pw.print(prefix); pw.print("lastActiveTime="); pw.print(lastActiveTime); pw.println(" (inactive for " + (getInactiveDuration() / 1000) + "s)"); pw.print(prefix); pw.println(" isTrimmable=" + mIsTrimmableFromRecents); @@ -5293,11 +5298,9 @@ class Task extends TaskFragment { // Place a new activity at top of root task, so it is next to interact with the user. if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_NO_ANIMATION) != 0) { - mDisplayContent.prepareAppTransition(TRANSIT_NONE); mTaskSupervisor.mNoAnimActivities.add(r); mTransitionController.setNoAnimation(r); } else { - mDisplayContent.prepareAppTransition(TRANSIT_OPEN); mTaskSupervisor.mNoAnimActivities.remove(r); } if (newTask && !r.mLaunchTaskBehind) { @@ -5477,7 +5480,8 @@ class Task extends TaskFragment { Slog.w(TAG, " Force finishing activity " + r.intent.getComponent().flattenToShortString()); Task finishedTask = r.getTask(); - mDisplayContent.requestTransitionAndLegacyPrepare(TRANSIT_CLOSE, TRANSIT_FLAG_APP_CRASHED); + mDisplayContent.requestTransitionAndLegacyPrepare(TRANSIT_CLOSE, TRANSIT_FLAG_APP_CRASHED, + finishedTask); r.finishIfPossible(reason, false /* oomAdj */); // Also terminate any activities below it that aren't yet stopped, to avoid a situation @@ -5695,7 +5699,6 @@ class Task extends TaskFragment { ActivityOptions.abort(options); } } - mDisplayContent.prepareAppTransition(transit); } final void moveTaskToFront(Task tr, boolean noAnimation, ActivityOptions options, @@ -5747,12 +5750,9 @@ class Task extends TaskFragment { if (DEBUG_TRANSITION) Slog.v(TAG_TRANSITION, "Prepare to front transition: task=" + tr); if (noAnimation) { - mDisplayContent.prepareAppTransition(TRANSIT_NONE); mTaskSupervisor.mNoAnimActivities.add(top); - if (mTransitionController.isShellTransitionsEnabled()) { - mTransitionController.collect(top); - mTransitionController.setNoAnimation(top); - } + mTransitionController.collect(top); + mTransitionController.setNoAnimation(top); ActivityOptions.abort(options); } else { updateTransitLocked(TRANSIT_TO_FRONT, options); @@ -5862,10 +5862,6 @@ class Task extends TaskFragment { moveTaskToBackInner(tr, transition); }); } else { - // Skip the transition for pinned task. - if (!inPinnedWindowingMode()) { - mDisplayContent.prepareAppTransition(TRANSIT_TO_BACK); - } moveTaskToBackInner(tr, null /* transition */); } return true; diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index a698a9e82929..80095b395cc4 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -44,10 +44,6 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND; -import static android.view.WindowManager.TRANSIT_NONE; -import static android.view.WindowManager.TRANSIT_OPEN; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_STATES; import static com.android.server.wm.ActivityRecord.State.PAUSED; @@ -56,7 +52,6 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityRecord.State.STOPPING; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RESULTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TRANSITION; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RESULTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_SWITCH; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TRANSITION; @@ -1682,36 +1677,15 @@ class TaskFragment extends WindowContainer<WindowContainer> { final DisplayContent dc = taskDisplayArea.mDisplayContent; if (prev != null) { if (prev.finishing) { - if (DEBUG_TRANSITION) { - Slog.v(TAG_TRANSITION, "Prepare close transition: prev=" + prev); - } if (mTaskSupervisor.mNoAnimActivities.contains(prev)) { anim = false; - dc.prepareAppTransition(TRANSIT_NONE); - } else { - dc.prepareAppTransition(TRANSIT_CLOSE); } prev.setVisibility(false); - } else { - if (DEBUG_TRANSITION) { - Slog.v(TAG_TRANSITION, "Prepare open transition: prev=" + prev); - } - if (mTaskSupervisor.mNoAnimActivities.contains(next)) { - anim = false; - dc.prepareAppTransition(TRANSIT_NONE); - } else { - dc.prepareAppTransition(TRANSIT_OPEN, - next.mLaunchTaskBehind ? TRANSIT_FLAG_OPEN_BEHIND : 0); - } - } - } else { - if (DEBUG_TRANSITION) Slog.v(TAG_TRANSITION, "Prepare open transition: no previous"); - if (mTaskSupervisor.mNoAnimActivities.contains(next)) { + } else if (mTaskSupervisor.mNoAnimActivities.contains(next)) { anim = false; - dc.prepareAppTransition(TRANSIT_NONE); - } else { - dc.prepareAppTransition(TRANSIT_OPEN); } + } else if (mTaskSupervisor.mNoAnimActivities.contains(next)) { + anim = false; } if (anim) { @@ -1910,14 +1884,17 @@ class TaskFragment extends WindowContainer<WindowContainer> { if (!hasDirectChildActivities()) { return false; } - if (mResumedActivity != null && mTransitionController.isTransientLaunch(mResumedActivity)) { + if (mResumedActivity != null && !mResumedActivity.finishing + && mTransitionController.isTransientLaunch(mResumedActivity)) { // Even if the transient activity is occluded, defer pausing (addToStopping will still // be called) it until the transient transition is done. So the current resuming // activity won't need to wait for additional pause complete. + ProtoLog.d(WM_DEBUG_STATES, "startPausing: Skipping pause for transient " + + "resumed activity=%s", mResumedActivity); return false; } - ProtoLog.d(WM_DEBUG_STATES, "startPausing: taskFrag =%s " + "mResumedActivity=%s", this, + ProtoLog.d(WM_DEBUG_STATES, "startPausing: taskFrag=%s mResumedActivity=%s", this, mResumedActivity); if (mPausingActivity != null) { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 5217a759c6ae..867b3a234f71 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1640,6 +1640,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mSyncEngine.setSyncMethod(syncId, BLASTSyncEngine.METHOD_BLAST); } mSyncEngine.addToSyncSet(syncId, target); + } else { + // If there is an existing sync group for the commit-at-end activity, + // enforce BLAST sync method for its windows, before resuming config dispatch. + target.forAllWindows(windowState -> { + windowState.mSyncMethodOverride = BLASTSyncEngine.METHOD_BLAST; + }, true /* traverseTopToBottom */); } // Reset surface state here (since it was skipped in buildFinishTransaction). Since // we are resuming config to the "current" state, we have to calculate the matching diff --git a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java index 242883612124..e612d8ec0ce6 100644 --- a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java +++ b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java @@ -20,6 +20,7 @@ import static android.graphics.Matrix.MSCALE_X; import static android.graphics.Matrix.MSCALE_Y; import static android.graphics.Matrix.MSKEW_X; import static android.graphics.Matrix.MSKEW_Y; +import static android.view.Display.INVALID_DISPLAY; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TPL; @@ -35,6 +36,7 @@ import android.util.ArrayMap; import android.util.IntArray; import android.util.Pair; import android.util.Size; +import android.util.SparseArray; import android.view.InputWindowHandle; import android.window.ITrustedPresentationListener; import android.window.TrustedPresentationThresholds; @@ -251,7 +253,7 @@ public class TrustedPresentationListenerController { Rect tmpLogicalDisplaySize = new Rect(); Matrix tmpInverseMatrix = new Matrix(); float[] tmpMatrix = new float[9]; - Region coveredRegionsAbove = new Region(); + SparseArray<Region> coveredRegionsAboveByDisplay = new SparseArray<>(); long currTimeMs = System.currentTimeMillis(); ProtoLog.v(WM_DEBUG_TPL, "Checking %d windows", mLastWindowHandles.first.length); @@ -262,7 +264,7 @@ public class TrustedPresentationListenerController { ProtoLog.v(WM_DEBUG_TPL, "Skipping %s", windowHandle.name); continue; } - var displayFound = false; + int displayId = INVALID_DISPLAY; tmpRectF.set(windowHandle.frame); for (var displayHandle : mLastWindowHandles.second) { if (displayHandle.mDisplayId == windowHandle.displayId) { @@ -273,17 +275,18 @@ public class TrustedPresentationListenerController { tmpLogicalDisplaySize.set(0, 0, displayHandle.mLogicalSize.getWidth(), displayHandle.mLogicalSize.getHeight()); tmpRect.intersect(tmpLogicalDisplaySize); - displayFound = true; + displayId = displayHandle.mDisplayId; break; } } - if (!displayFound) { + if (displayId == INVALID_DISPLAY) { ProtoLog.v(WM_DEBUG_TPL, "Skipping %s, no associated display %d", windowHandle.name, windowHandle.displayId); continue; } + Region coveredRegionsAbove = coveredRegionsAboveByDisplay.get(displayId, new Region()); var listeners = mRegisteredListeners.get(windowHandle.getWindowToken()); if (listeners != null) { Region region = new Region(); @@ -304,6 +307,7 @@ public class TrustedPresentationListenerController { } coveredRegionsAbove.op(tmpRect, Region.Op.UNION); + coveredRegionsAboveByDisplay.put(displayId, coveredRegionsAbove); ProtoLog.v(WM_DEBUG_TPL, "coveredRegionsAbove updated with %s frame:%s region:%s", windowHandle.name, tmpRect.toShortString(), coveredRegionsAbove); } diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 7c88abcec7ec..9506ffeb2792 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -217,8 +217,7 @@ class WallpaperWindowToken extends WindowToken { } // If in a transition, defer commits for activities that are going invisible - if (!visible && (mTransitionController.inTransition() - || getDisplayContent().mAppTransition.isRunning())) { + if (!visible && mTransitionController.inTransition()) { return; } diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 45202a29ba97..d0d2067ac4bc 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -16,9 +16,6 @@ package com.android.server.wm; -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.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; @@ -29,24 +26,15 @@ import static android.content.pm.ActivityInfo.reverseOrientation; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; -import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.os.UserHandle.USER_NULL; import static android.view.SurfaceControl.Transaction; import static android.view.WindowInsets.Type.InsetsType; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; -import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR; import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS_ANIM; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ORIENTATION; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_SYNC_ENGINE; -import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; -import static com.android.server.wm.AppTransition.MAX_APP_TRANSITION_DURATION; -import static com.android.server.wm.AppTransition.isActivityTransitOld; -import static com.android.server.wm.AppTransition.isTaskFragmentTransitOld; -import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; 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; @@ -65,7 +53,6 @@ import static com.android.server.wm.WindowContainerProto.VISIBLE; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG; 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.WindowStateAnimator.ROOT_TASK_CLIP_AFTER_ANIM; import android.annotation.CallSuper; import android.annotation.ColorInt; @@ -81,10 +68,8 @@ import android.graphics.Rect; import android.os.Debug; import android.os.IBinder; import android.os.RemoteException; -import android.os.Trace; import android.util.ArrayMap; import android.util.ArraySet; -import android.util.Pair; import android.util.Pools; import android.util.RotationUtils; import android.util.Slog; @@ -102,14 +87,11 @@ import android.view.SurfaceControl.Builder; import android.view.SurfaceControlViewHost; import android.view.WindowManager; import android.view.WindowManager.TransitionOldType; -import android.view.animation.Animation; import android.window.IWindowContainerToken; import android.window.WindowContainerToken; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.graphics.ColorUtils; import com.android.internal.protolog.ProtoLog; -import com.android.internal.protolog.common.LogLevel; import com.android.internal.util.ToBooleanFunction; import com.android.server.wm.SurfaceAnimator.Animatable; import com.android.server.wm.SurfaceAnimator.AnimationType; @@ -1315,31 +1297,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } /** - * Returns true if the container or one of its children as some content it can display or wants - * to display (e.g. app views or saved surface). - * - * NOTE: While this method will return true if the there is some content to display, it doesn't - * mean the container is visible. Use {@link #isVisible()} to determine if the container is - * visible. - */ - boolean hasContentToDisplay() { - for (int i = mChildren.size() - 1; i >= 0; --i) { - final WindowContainer wc = mChildren.get(i); - if (wc.hasContentToDisplay()) { - return true; - } - } - return false; - } - - /** * Returns true if the container or one of its children is considered visible from the * WindowManager perspective which usually means valid surface and some other internal state * are true. * * NOTE: While this method will return true if the surface is visible, it doesn't mean the - * client has actually displayed any content. Use {@link #hasContentToDisplay()} to determine if - * the container has any content to display. + * client has actually displayed any content. */ boolean isVisible() { // TODO: Will this be more correct if it checks the visibility of its parents? @@ -1480,13 +1443,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } } - void onAppTransitionDone() { - for (int i = mChildren.size() - 1; i >= 0; --i) { - final WindowContainer wc = mChildren.get(i); - wc.onAppTransitionDone(); - } - } - /** * Called when this container or one of its descendants changed its requested orientation, and * wants this container to handle it or pass it to its parent. @@ -3039,264 +2995,10 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< getRelativePosition(outPosition); } - /** - * Applies the app transition animation according the given the layout properties in the - * window hierarchy. - * - * @param lp The layout parameters of the window. - * @param transit The app transition type indicates what kind of transition to be applied. - * @param enter Whether the app transition is entering transition or not. - * @param isVoiceInteraction Whether the container is participating in voice interaction or not. - * @param sources {@link ActivityRecord}s which causes this app transition animation. - * - * @return {@code true} when the container applied the app transition, {@code false} if the - * app transition is disabled or skipped. - * - * @see #getAnimationAdapter - */ - boolean applyAnimation(WindowManager.LayoutParams lp, @TransitionOldType int transit, - boolean enter, boolean isVoiceInteraction, - @Nullable ArrayList<WindowContainer> sources) { - if (mWmService.mDisableTransitionAnimation) { - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: transition animation is disabled or skipped. " - + "container=%s", this); - cancelAnimation(); - return false; - } - - // Only apply an animation if the display isn't frozen. If it is frozen, there is no reason - // to animate and it can cause strange artifacts when we unfreeze the display if some - // different animation is running. - try { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WC#applyAnimation"); - if (okToAnimate()) { - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: transit=%s, enter=%b, wc=%s", - AppTransition.appTransitionOldToString(transit), enter, this); - applyAnimationUnchecked(lp, enter, transit, isVoiceInteraction, sources); - } else { - cancelAnimation(); - } - } finally { - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - } - - return isAnimating(); - } - - /** - * Gets the {@link AnimationAdapter} according the given window layout properties in the window - * hierarchy. - * - * @return The return value will always contain two elements, one for normal animations and the - * other for thumbnail animation, both can be {@code null}. - * - * @See com.android.server.wm.RemoteAnimationController.RemoteAnimationRecord - * @See LocalAnimationAdapter - */ - Pair<AnimationAdapter, AnimationAdapter> getAnimationAdapter(WindowManager.LayoutParams lp, - @TransitionOldType int transit, boolean enter, boolean isVoiceInteraction) { - final Pair<AnimationAdapter, AnimationAdapter> resultAdapters; - final int appRootTaskClipMode = getDisplayContent().mAppTransition.getAppRootTaskClipMode(); - - // Separate position and size for use in animators. - final Rect screenBounds = getAnimationBounds(appRootTaskClipMode); - mTmpRect.set(screenBounds); - getAnimationPosition(mTmpPoint); - mTmpRect.offsetTo(0, 0); - - final boolean isChanging = AppTransition.isChangeTransitOld(transit); - - if (isChanging) { - final float durationScale = mWmService.getTransitionAnimationScaleLocked(); - final DisplayInfo displayInfo = getDisplayContent().getDisplayInfo(); - mTmpRect.offsetTo(mTmpPoint.x, mTmpPoint.y); - - final AnimationAdapter adapter = new LocalAnimationAdapter( - new WindowChangeAnimationSpec(null /* startBounds */, mTmpRect, - displayInfo, durationScale, true /* isAppAnimation */, - false /* isThumbnail */), - getSurfaceAnimationRunner()); - - final AnimationAdapter thumbnailAdapter = null; - resultAdapters = new Pair<>(adapter, thumbnailAdapter); - mTransit = transit; - mTransitFlags = getDisplayContent().mAppTransition.getTransitFlags(); - } else { - mNeedsAnimationBoundsLayer = (appRootTaskClipMode == ROOT_TASK_CLIP_AFTER_ANIM); - final Animation a = loadAnimation(lp, transit, enter, isVoiceInteraction); - - if (a != null) { - // Only apply corner radius to animation if we're not in multi window mode. - // We don't want rounded corners when in pip or split screen. - final float windowCornerRadius = !inMultiWindowMode() - ? getDisplayContent().getWindowCornerRadius() - : 0; - if (asActivityRecord() != null - && asActivityRecord().isNeedsLetterboxedAnimation()) { - asActivityRecord().getLetterboxInnerBounds(mTmpRect); - } - AnimationAdapter adapter = new LocalAnimationAdapter( - new WindowAnimationSpec(a, mTmpPoint, mTmpRect, - getDisplayContent().mAppTransition.canSkipFirstFrame(), - appRootTaskClipMode, true /* isAppAnimation */, windowCornerRadius), - getSurfaceAnimationRunner()); - - resultAdapters = new Pair<>(adapter, null); - mNeedsZBoost = a.getZAdjustment() == Animation.ZORDER_TOP - || AppTransition.isClosingTransitOld(transit); - mTransit = transit; - mTransitFlags = getDisplayContent().mAppTransition.getTransitFlags(); - } else { - resultAdapters = new Pair<>(null, null); - } - } - return resultAdapters; - } - - protected void applyAnimationUnchecked(WindowManager.LayoutParams lp, boolean enter, - @TransitionOldType int transit, boolean isVoiceInteraction, - @Nullable ArrayList<WindowContainer> sources) { - final Task task = asTask(); - if (task != null && !enter && !task.isActivityTypeHomeOrRecents()) { - final InsetsControlTarget imeTarget = mDisplayContent.getImeTarget(IME_TARGET_LAYERING); - final boolean isImeLayeringTarget = imeTarget != null && imeTarget.getWindow() != null - && imeTarget.getWindow().getTask() == task; - // Attach and show the IME screenshot when the task is the IME target and performing - // task closing transition to the next task. - if (isImeLayeringTarget && AppTransition.isTaskCloseTransitOld(transit)) { - mDisplayContent.showImeScreenshot(); - } - } - final Pair<AnimationAdapter, AnimationAdapter> adapters = getAnimationAdapter(lp, - transit, enter, isVoiceInteraction); - AnimationAdapter adapter = adapters.first; - AnimationAdapter thumbnailAdapter = adapters.second; - if (adapter != null) { - if (sources != null) { - mSurfaceAnimationSources.addAll(sources); - } - - AnimationRunnerBuilder animationRunnerBuilder = new AnimationRunnerBuilder(); - - // Check if the animation requests to show background color for Activity and embedded - // TaskFragment. - final ActivityRecord activityRecord = asActivityRecord(); - final TaskFragment taskFragment = asTaskFragment(); - if (adapter.getShowBackground() - // Check if it is Activity transition. - && ((activityRecord != null && isActivityTransitOld(transit)) - // Check if it is embedded TaskFragment transition. - || (taskFragment != null && taskFragment.isEmbedded() - && isTaskFragmentTransitOld(transit)))) { - final @ColorInt int backgroundColorForTransition; - if (adapter.getBackgroundColor() != 0) { - // If available use the background color provided through getBackgroundColor - // which if set originates from a call to overridePendingAppTransition. - backgroundColorForTransition = adapter.getBackgroundColor(); - } else { - final TaskFragment organizedTf = activityRecord != null - ? activityRecord.getOrganizedTaskFragment() - : taskFragment.getOrganizedTaskFragment(); - if (organizedTf != null && organizedTf.getAnimationParams() - .getAnimationBackgroundColor() != DEFAULT_ANIMATION_BACKGROUND_COLOR) { - // This window is embedded and has an animation background color set on the - // TaskFragment. Pass this color with this window, so the handler can use it - // as the animation background color if needed, - backgroundColorForTransition = organizedTf.getAnimationParams() - .getAnimationBackgroundColor(); - } else { - // Otherwise default to the window's background color if provided through - // the theme as the background color for the animation - the top most window - // with a valid background color and showBackground set takes precedence. - final Task parentTask = activityRecord != null - ? activityRecord.getTask() - : taskFragment.getTask(); - backgroundColorForTransition = parentTask.getTaskDescription() - .getBackgroundColor(); - } - } - // Set to opaque for animation background to prevent it from exposing the blank - // background or content below. - animationRunnerBuilder.setTaskBackgroundColor(ColorUtils.setAlphaComponent( - backgroundColorForTransition, 255)); - } - - animationRunnerBuilder.build() - .startAnimation(getPendingTransaction(), adapter, !isVisible(), - ANIMATION_TYPE_APP_TRANSITION, thumbnailAdapter); - - if (adapter.getShowWallpaper()) { - getDisplayContent().pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; - } - } - } - final SurfaceAnimationRunner getSurfaceAnimationRunner() { return mWmService.mSurfaceAnimationRunner; } - private Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter, - boolean isVoiceInteraction) { - if ((isOrganized() - // TODO(b/161711458): Clean-up when moved to shell. - && getWindowingMode() != WINDOWING_MODE_FULLSCREEN - && getWindowingMode() != WINDOWING_MODE_FREEFORM - && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW)) { - return null; - } - - final DisplayContent displayContent = getDisplayContent(); - final DisplayInfo displayInfo = displayContent.getDisplayInfo(); - final int width = displayInfo.appWidth; - final int height = displayInfo.appHeight; - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, "applyAnimation: container=%s", this); - - // Determine the visible rect to calculate the thumbnail clip with - // getAnimationFrames. - final Rect frame = new Rect(0, 0, width, height); - final Rect displayFrame = new Rect(0, 0, - displayInfo.logicalWidth, displayInfo.logicalHeight); - final Rect insets = new Rect(); - final Rect stableInsets = new Rect(); - final Rect surfaceInsets = new Rect(); - getAnimationFrames(frame, insets, stableInsets, surfaceInsets); - - if (mLaunchTaskBehind) { - // Differentiate the two animations. This one which is briefly on the screen - // gets the !enter animation, and the other one which remains on the - // screen gets the enter animation. Both appear in the mOpeningApps set. - enter = false; - } - ProtoLog.d(WM_DEBUG_APP_TRANSITIONS, - "Loading animation for app transition. transit=%s enter=%b frame=%s insets=%s " - + "surfaceInsets=%s", - AppTransition.appTransitionOldToString(transit), enter, frame, insets, - surfaceInsets); - final Configuration displayConfig = displayContent.getConfiguration(); - final Animation a = getDisplayContent().mAppTransition.loadAnimation(lp, transit, enter, - displayConfig.uiMode, displayConfig.orientation, frame, displayFrame, insets, - surfaceInsets, stableInsets, isVoiceInteraction, inFreeformWindowingMode(), this); - if (a != null) { - if (a != null) { - // Setup the maximum app transition duration to prevent malicious app may set a long - // animation duration or infinite repeat counts for the app transition through - // ActivityOption#makeCustomAnimation or WindowManager#overridePendingTransition. - a.restrictDuration(MAX_APP_TRANSITION_DURATION); - } - if (ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.DEBUG)) { - ProtoLog.i(WM_DEBUG_ANIM, "Loaded animation %s for %s, duration: %d, stack=%s", - a, this, ((a != null) ? a.getDuration() : 0), Debug.getCallers(20)); - } - final int containingWidth = frame.width(); - final int containingHeight = frame.height(); - a.initialize(containingWidth, containingHeight, width, height); - a.scaleCurrentDuration(mWmService.getTransitionAnimationScaleLocked()); - } - return a; - } - boolean canCreateRemoteAnimationTarget() { return false; } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index d699a689459e..8aed91b2dc66 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -157,6 +157,7 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY; import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER; import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID; import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; +import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.multiCrop; import static com.android.window.flags.Flags.setScPropertiesInClient; @@ -1820,125 +1821,158 @@ public class WindowManagerService extends IWindowManager.Stub final boolean hideSystemAlertWindows = shouldHideNonSystemOverlayWindow(win); win.setForceHideNonSystemOverlayWindowIfNeeded(hideSystemAlertWindows); - boolean imMayMove = true; - - win.mToken.addWindow(win); - displayPolicy.addWindowLw(win, attrs); - displayPolicy.setDropInputModePolicy(win, win.mAttrs); - if (type == TYPE_APPLICATION_STARTING && activity != null) { - activity.attachStartingWindow(win); - ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "addWindow: %s startingWindow=%s", - activity, win); - } else if (type == TYPE_INPUT_METHOD - // IME window is always touchable. - // Ignore non-touchable windows e.g. Stylus InkWindow.java. - && (win.getAttrs().flags & FLAG_NOT_TOUCHABLE) == 0) { - displayContent.setInputMethodWindowLocked(win); - imMayMove = false; - } else if (type == TYPE_INPUT_METHOD_DIALOG) { - displayContent.computeImeTarget(true /* updateImeTarget */); - imMayMove = false; - } else { - if (type == TYPE_WALLPAPER) { - displayContent.mWallpaperController.clearLastWallpaperTimeoutTime(); - displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; - } else if (win.hasWallpaper()) { - displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; - } else if (displayContent.mWallpaperController.isBelowWallpaperTarget(win)) { - // If there is currently a wallpaper being shown, and - // the base layer of the new window is below the current - // layer of the target window, then adjust the wallpaper. - // This is to avoid a new window being placed between the - // wallpaper and its target. - displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; + // Only a presentation window needs a transition because its visibility affets the + // lifecycle of apps below (b/390481865). + if (enablePresentationForConnectedDisplays() && win.isPresentation()) { + Transition transition = null; + if (!win.mTransitionController.isCollecting()) { + transition = win.mTransitionController.createAndStartCollecting(TRANSIT_OPEN); + } + win.mTransitionController.collect(win.mToken); + res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState, + outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs); + // A presentation hides all activities behind on the same display. + win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + win.mTransitionController.getCollectingTransition().setReady(win.mToken, true); + if (transition != null) { + win.mTransitionController.requestStartTransition(transition, null, + null /* remoteTransition */, null /* displayChange */); } + } else { + res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState, + outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs); } + } - final WindowStateAnimator winAnimator = win.mWinAnimator; - winAnimator.mEnterAnimationPending = true; - winAnimator.mEnteringAnimation = true; + Binder.restoreCallingIdentity(origId); - if (displayPolicy.areSystemBarsForcedConsumedLw()) { - res |= WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS; - } - if (displayContent.isInTouchMode()) { - res |= WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE; - } - if (win.mActivityRecord == null || win.mActivityRecord.isClientVisible()) { - res |= WindowManagerGlobal.ADD_FLAG_APP_VISIBLE; + return res; + } + + private int addWindowInner(@NonNull WindowState win, @NonNull DisplayPolicy displayPolicy, + @NonNull ActivityRecord activity, @NonNull DisplayContent displayContent, + @NonNull InsetsState outInsetsState, @NonNull Rect outAttachedFrame, + @NonNull InsetsSourceControl.Array outActiveControls, @NonNull IWindow client, + @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs) { + int res = 0; + final int type = attrs.type; + boolean imMayMove = true; + + win.mToken.addWindow(win); + displayPolicy.addWindowLw(win, attrs); + displayPolicy.setDropInputModePolicy(win, win.mAttrs); + if (type == TYPE_APPLICATION_STARTING && activity != null) { + activity.attachStartingWindow(win); + ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "addWindow: %s startingWindow=%s", + activity, win); + } else if (type == TYPE_INPUT_METHOD + // IME window is always touchable. + // Ignore non-touchable windows e.g. Stylus InkWindow.java. + && (win.getAttrs().flags & FLAG_NOT_TOUCHABLE) == 0) { + displayContent.setInputMethodWindowLocked(win); + imMayMove = false; + } else if (type == TYPE_INPUT_METHOD_DIALOG) { + displayContent.computeImeTarget(true /* updateImeTarget */); + imMayMove = false; + } else { + if (type == TYPE_WALLPAPER) { + displayContent.mWallpaperController.clearLastWallpaperTimeoutTime(); + displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; + } else if (win.hasWallpaper()) { + displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; + } else if (displayContent.mWallpaperController.isBelowWallpaperTarget(win)) { + // If there is currently a wallpaper being shown, and + // the base layer of the new window is below the current + // layer of the target window, then adjust the wallpaper. + // This is to avoid a new window being placed between the + // wallpaper and its target. + displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; } + } - displayContent.getInputMonitor().setUpdateInputWindowsNeededLw(); + final WindowStateAnimator winAnimator = win.mWinAnimator; + winAnimator.mEnterAnimationPending = true; + winAnimator.mEnteringAnimation = true; - boolean focusChanged = false; - if (win.canReceiveKeys()) { - focusChanged = updateFocusedWindowLocked(UPDATE_FOCUS_WILL_ASSIGN_LAYERS, - false /*updateInputWindows*/); - if (focusChanged) { - imMayMove = false; - } - } + if (displayPolicy.areSystemBarsForcedConsumedLw()) { + res |= WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS; + } + if (displayContent.isInTouchMode()) { + res |= WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE; + } + if (win.mActivityRecord == null || win.mActivityRecord.isClientVisible()) { + res |= WindowManagerGlobal.ADD_FLAG_APP_VISIBLE; + } - if (imMayMove) { - displayContent.computeImeTarget(true /* updateImeTarget */); - if (win.isImeOverlayLayeringTarget()) { - dispatchImeTargetOverlayVisibilityChanged(client.asBinder(), win.mAttrs.type, - win.isVisibleRequestedOrAdding(), false /* removed */, - displayContent.getDisplayId()); - } - } + displayContent.getInputMonitor().setUpdateInputWindowsNeededLw(); - // Don't do layout here, the window must call - // relayout to be displayed, so we'll do it there. - if (win.mActivityRecord != null && win.mActivityRecord.isEmbedded()) { - // Assign child layers from the parent Task if the Activity is embedded. - win.getTask().assignChildLayers(); - } else { - win.getParent().assignChildLayers(); + boolean focusChanged = false; + if (win.canReceiveKeys()) { + focusChanged = updateFocusedWindowLocked(UPDATE_FOCUS_WILL_ASSIGN_LAYERS, + false /*updateInputWindows*/); + if (focusChanged) { + imMayMove = false; } + } - if (focusChanged) { - displayContent.getInputMonitor().setInputFocusLw(displayContent.mCurrentFocus, - false /*updateInputWindows*/); + if (imMayMove) { + displayContent.computeImeTarget(true /* updateImeTarget */); + if (win.isImeOverlayLayeringTarget()) { + dispatchImeTargetOverlayVisibilityChanged(client.asBinder(), win.mAttrs.type, + win.isVisibleRequestedOrAdding(), false /* removed */, + displayContent.getDisplayId()); } - displayContent.getInputMonitor().updateInputWindowsLw(false /*force*/); + } - ProtoLog.v(WM_DEBUG_ADD_REMOVE, "addWindow: New client %s" - + ": window=%s Callers=%s", client.asBinder(), win, Debug.getCallers(5)); + // Don't do layout here, the window must call + // relayout to be displayed, so we'll do it there. + if (win.mActivityRecord != null && win.mActivityRecord.isEmbedded()) { + // Assign child layers from the parent Task if the Activity is embedded. + win.getTask().assignChildLayers(); + } else { + win.getParent().assignChildLayers(); + } - boolean needToSendNewConfiguration = - win.isVisibleRequestedOrAdding() && displayContent.updateOrientation(); - if (win.providesDisplayDecorInsets()) { - needToSendNewConfiguration |= displayPolicy.updateDecorInsetsInfo(); - } - if (needToSendNewConfiguration) { - displayContent.sendNewConfiguration(); - } + if (focusChanged) { + displayContent.getInputMonitor().setInputFocusLw(displayContent.mCurrentFocus, + false /*updateInputWindows*/); + } + displayContent.getInputMonitor().updateInputWindowsLw(false /*force*/); - // This window doesn't have a frame yet. Don't let this window cause the insets change. - displayContent.getInsetsStateController().updateAboveInsetsState( - false /* notifyInsetsChanged */); + ProtoLog.v(WM_DEBUG_ADD_REMOVE, "addWindow: New client %s" + + ": window=%s Callers=%s", client.asBinder(), win, Debug.getCallers(5)); - win.fillInsetsState(outInsetsState, true /* copySources */); - getInsetsSourceControls(win, outActiveControls); + boolean needToSendNewConfiguration = + win.isVisibleRequestedOrAdding() && displayContent.updateOrientation(); + if (win.providesDisplayDecorInsets()) { + needToSendNewConfiguration |= displayPolicy.updateDecorInsetsInfo(); + } + if (needToSendNewConfiguration) { + displayContent.sendNewConfiguration(); + } - if (win.mLayoutAttached) { - outAttachedFrame.set(win.getParentWindow().getFrame()); - if (win.mInvGlobalScale != 1f) { - outAttachedFrame.scale(win.mInvGlobalScale); - } - } else { - // Make this invalid which indicates a null attached frame. - outAttachedFrame.set(0, 0, -1, -1); - } - outSizeCompatScale[0] = win.getCompatScaleForClient(); + // This window doesn't have a frame yet. Don't let this window cause the insets change. + displayContent.getInsetsStateController().updateAboveInsetsState( + false /* notifyInsetsChanged */); + + win.fillInsetsState(outInsetsState, true /* copySources */); + getInsetsSourceControls(win, outActiveControls); - if (res >= ADD_OKAY && win.isPresentation()) { - mPresentationController.onPresentationAdded(win); + if (win.mLayoutAttached) { + outAttachedFrame.set(win.getParentWindow().getFrame()); + if (win.mInvGlobalScale != 1f) { + outAttachedFrame.scale(win.mInvGlobalScale); } + } else { + // Make this invalid which indicates a null attached frame. + outAttachedFrame.set(0, 0, -1, -1); } + outSizeCompatScale[0] = win.getCompatScaleForClient(); - Binder.restoreCallingIdentity(origId); + if (res >= ADD_OKAY && win.isPresentation()) { + mPresentationController.onPresentationAdded(win); + } return res; } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 30dde543b9d4..270de0197a4e 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -37,6 +37,7 @@ import static com.android.server.wm.ActivityRecord.State.PAUSING; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityRecord.State.STARTED; import static com.android.server.wm.ActivityRecord.State.STOPPING; +import static com.android.server.wm.ActivityRecord.State.STOPPED; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RELEASE; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_CONFIGURATION; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RELEASE; @@ -344,6 +345,12 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio */ private volatile int mActivityStateFlags = ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER; + /** + * The most recent timestamp of when one of this process's stopped activities in a + * perceptible task became stopped. Written by window manager and read by activity manager. + */ + private volatile long mPerceptibleTaskStoppedTimeMillis = Long.MIN_VALUE; + public WindowProcessController(@NonNull ActivityTaskManagerService atm, @NonNull ApplicationInfo info, String name, int uid, int userId, Object owner, @NonNull WindowProcessListener listener) { @@ -475,8 +482,9 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio r.detachFromProcess(); if (r.isVisibleRequested()) { hasVisibleActivity = true; + Task finishingTask = r.getTask(); r.mDisplayContent.requestTransitionAndLegacyPrepare(TRANSIT_CLOSE, - TRANSIT_FLAG_APP_CRASHED); + TRANSIT_FLAG_APP_CRASHED, finishingTask); } r.destroyIfPossible("handleAppCrashed"); } @@ -1228,6 +1236,17 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio return mActivityStateFlags; } + /** + * Returns the most recent timestamp when one of this process's stopped activities in a + * perceptible task became stopped. It should only be called if {@link #hasActivities} + * returns {@code true} and {@link #getActivityStateFlags} does not have any of + * the ACTIVITY_STATE_FLAG_IS_(VISIBLE|PAUSING_OR_PAUSED|STOPPING) bit set. + */ + @HotPath(caller = HotPath.OOM_ADJUSTMENT) + public long getPerceptibleTaskStoppedTimeMillis() { + return mPerceptibleTaskStoppedTimeMillis; + } + void computeProcessActivityState() { // Since there could be more than one activities in a process record, we don't need to // compute the OomAdj with each of them, just need to find out the activity with the @@ -1239,6 +1258,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio int minTaskLayer = Integer.MAX_VALUE; int stateFlags = 0; int nonOccludedRatio = 0; + long perceptibleTaskStoppedTimeMillis = Long.MIN_VALUE; final boolean wasResumed = hasResumedActivity(); final boolean wasAnyVisible = (mActivityStateFlags & (ACTIVITY_STATE_FLAG_IS_VISIBLE | ACTIVITY_STATE_FLAG_IS_WINDOW_VISIBLE)) != 0; @@ -1287,6 +1307,11 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio bestInvisibleState = STOPPING; // Not "finishing" if any of activity isn't finishing. allStoppingFinishing &= r.finishing; + } else if (bestInvisibleState == DESTROYED && r.isState(STOPPED)) { + if (task.mIsPerceptible) { + perceptibleTaskStoppedTimeMillis = + Long.max(r.mStoppedTime, perceptibleTaskStoppedTimeMillis); + } } } } @@ -1324,6 +1349,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } } mActivityStateFlags = stateFlags; + mPerceptibleTaskStoppedTimeMillis = perceptibleTaskStoppedTimeMillis; final boolean anyVisible = (stateFlags & (ACTIVITY_STATE_FLAG_IS_VISIBLE | ACTIVITY_STATE_FLAG_IS_WINDOW_VISIBLE)) != 0; diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 9f1289b2c12a..bfedc90497ae 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -95,6 +95,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; @@ -182,6 +183,7 @@ import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_ARE import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY; import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER; import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES; +import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.surfaceTrustedOverlay; import android.annotation.CallSuper; @@ -1696,17 +1698,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP || mActivityRecord.isStartingWindowDisplayed()); } - @Override - boolean hasContentToDisplay() { - if (!isDrawn() && (mViewVisibility == View.VISIBLE - || (isAnimating(TRANSITION | PARENTS) - && !getDisplayContent().mAppTransition.isTransitionSet()))) { - return true; - } - - return super.hasContentToDisplay(); - } - private boolean isVisibleByPolicyOrInsets() { return isVisibleByPolicy() // If we don't have a provider, this window isn't used as a window generating @@ -2297,11 +2288,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP dc.updateImeInputAndControlTarget(null); } - final int type = mAttrs.type; - - if (isPresentation()) { - mWmService.mPresentationController.onPresentationRemoved(this); - } // Check if window provides non decor insets before clearing its provided insets. final boolean windowProvidesDisplayDecorInsets = providesDisplayDecorInsets(); @@ -2442,11 +2428,33 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } } - removeImmediately(); - mWmService.updateFocusedWindowLocked(isFocused() - ? UPDATE_FOCUS_REMOVING_FOCUS - : UPDATE_FOCUS_NORMAL, - true /*updateInputWindows*/); + // Only a presentation window needs a transition because its visibility affets the + // lifecycle of apps below (b/390481865). + if (enablePresentationForConnectedDisplays() && isPresentation()) { + Transition transition = null; + if (!mTransitionController.isCollecting()) { + transition = mTransitionController.createAndStartCollecting(TRANSIT_CLOSE); + } + mTransitionController.collect(mToken); + mAnimatingExit = true; + mRemoveOnExit = true; + mToken.setVisibleRequested(false); + mWmService.mPresentationController.onPresentationRemoved(this); + // A presentation hides all activities behind on the same display. + mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + mTransitionController.getCollectingTransition().setReady(mToken, true); + if (transition != null) { + mTransitionController.requestStartTransition(transition, null, + null /* remoteTransition */, null /* displayChange */); + } + } else { + removeImmediately(); + mWmService.updateFocusedWindowLocked(isFocused() + ? UPDATE_FOCUS_REMOVING_FOCUS + : UPDATE_FOCUS_NORMAL, + true /*updateInputWindows*/); + } } finally { Binder.restoreCallingIdentity(origId); } diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index d26539c377a9..95776088aad8 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -204,6 +204,7 @@ cc_defaults { "android.frameworks.sensorservice-V1-ndk", "android.frameworks.stats@1.0", "android.frameworks.stats-V2-ndk", + "android.os.vibrator.flags-aconfig-cc", "android.system.suspend.control-V1-cpp", "android.system.suspend.control.internal-cpp", "android.system.suspend-V1-ndk", diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp index abd4cd25cf68..534dbb1f6cf1 100644 --- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp +++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp @@ -21,6 +21,7 @@ #include <android/binder_parcel_jni.h> #include <android/hardware/vibrator/1.3/IVibrator.h> #include <android/persistable_bundle_aidl.h> +#include <android_os_vibrator.h> #include <nativehelper/JNIHelp.h> #include <utils/Log.h> #include <utils/misc.h> @@ -143,21 +144,23 @@ public: return mHal->doWithRetry(fn, functionName); } - std::function<void()> createCallback(jlong vibrationId) { + std::function<void()> createCallback(jlong vibrationId, jlong stepId) { auto callbackId = ++mCallbackId; - return [vibrationId, callbackId, this]() { + return [vibrationId, stepId, callbackId, this]() { auto currentCallbackId = mCallbackId.load(); - if (currentCallbackId != callbackId) { - // This callback is from an older HAL call that is no longer relevant to the service + if (!android_os_vibrator_fix_vibration_thread_callback_handling() && + currentCallbackId != callbackId) { + // This callback is from an older HAL call that is no longer relevant return; } auto jniEnv = GetOrAttachJNIEnvironment(sJvm); - jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnComplete, mVibratorId, - vibrationId); + jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnComplete, mVibratorId, vibrationId, + stepId); }; } void disableOldCallbacks() { + // TODO remove this once android_os_vibrator_fix_vibration_thread_callback_handling removed mCallbackId++; } @@ -165,6 +168,7 @@ private: const std::shared_ptr<vibrator::HalController> mHal; const int32_t mVibratorId; const jobject mCallbackListener; + // TODO remove this once android_os_vibrator_fix_vibration_thread_callback_handling removed std::atomic<int64_t> mCallbackId; }; @@ -273,13 +277,13 @@ static jboolean vibratorIsAvailable(JNIEnv* env, jclass /* clazz */, jlong ptr) } static jlong vibratorOn(JNIEnv* env, jclass /* clazz */, jlong ptr, jlong timeoutMs, - jlong vibrationId) { + jlong vibrationId, jlong stepId) { VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr); if (wrapper == nullptr) { ALOGE("vibratorOn failed because native wrapper was not initialized"); return -1; } - auto callback = wrapper->createCallback(vibrationId); + auto callback = wrapper->createCallback(vibrationId, stepId); auto onFn = [timeoutMs, &callback](vibrator::HalWrapper* hal) { return hal->on(std::chrono::milliseconds(timeoutMs), callback); }; @@ -324,7 +328,7 @@ static void vibratorSetExternalControl(JNIEnv* env, jclass /* clazz */, jlong pt } static jlong vibratorPerformEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, jlong effect, - jlong strength, jlong vibrationId) { + jlong strength, jlong vibrationId, jlong stepId) { VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr); if (wrapper == nullptr) { ALOGE("vibratorPerformEffect failed because native wrapper was not initialized"); @@ -332,7 +336,7 @@ static jlong vibratorPerformEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, j } Aidl::Effect effectType = static_cast<Aidl::Effect>(effect); Aidl::EffectStrength effectStrength = static_cast<Aidl::EffectStrength>(strength); - auto callback = wrapper->createCallback(vibrationId); + auto callback = wrapper->createCallback(vibrationId, stepId); auto performEffectFn = [effectType, effectStrength, &callback](vibrator::HalWrapper* hal) { return hal->performEffect(effectType, effectStrength, callback); }; @@ -342,7 +346,7 @@ static jlong vibratorPerformEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, j static jlong vibratorPerformVendorEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, jobject vendorData, jlong strength, jfloat scale, - jfloat adaptiveScale, jlong vibrationId) { + jfloat adaptiveScale, jlong vibrationId, jlong stepId) { VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr); if (wrapper == nullptr) { ALOGE("vibratorPerformVendorEffect failed because native wrapper was not initialized"); @@ -350,7 +354,7 @@ static jlong vibratorPerformVendorEffect(JNIEnv* env, jclass /* clazz */, jlong } Aidl::VendorEffect effect = vendorEffectFromJavaParcel(env, vendorData, strength, scale, adaptiveScale); - auto callback = wrapper->createCallback(vibrationId); + auto callback = wrapper->createCallback(vibrationId, stepId); auto performVendorEffectFn = [&effect, &callback](vibrator::HalWrapper* hal) { return hal->performVendorEffect(effect, callback); }; @@ -359,7 +363,8 @@ static jlong vibratorPerformVendorEffect(JNIEnv* env, jclass /* clazz */, jlong } static jlong vibratorPerformComposedEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, - jobjectArray composition, jlong vibrationId) { + jobjectArray composition, jlong vibrationId, + jlong stepId) { VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr); if (wrapper == nullptr) { ALOGE("vibratorPerformComposedEffect failed because native wrapper was not initialized"); @@ -371,7 +376,7 @@ static jlong vibratorPerformComposedEffect(JNIEnv* env, jclass /* clazz */, jlon jobject element = env->GetObjectArrayElement(composition, i); effects.push_back(effectFromJavaPrimitive(env, element)); } - auto callback = wrapper->createCallback(vibrationId); + auto callback = wrapper->createCallback(vibrationId, stepId); auto performComposedEffectFn = [&effects, &callback](vibrator::HalWrapper* hal) { return hal->performComposedEffect(effects, callback); }; @@ -381,7 +386,8 @@ static jlong vibratorPerformComposedEffect(JNIEnv* env, jclass /* clazz */, jlon } static jlong vibratorPerformPwleEffect(JNIEnv* env, jclass /* clazz */, jlong ptr, - jobjectArray waveform, jint brakingId, jlong vibrationId) { + jobjectArray waveform, jint brakingId, jlong vibrationId, + jlong stepId) { VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr); if (wrapper == nullptr) { ALOGE("vibratorPerformPwleEffect failed because native wrapper was not initialized"); @@ -406,7 +412,7 @@ static jlong vibratorPerformPwleEffect(JNIEnv* env, jclass /* clazz */, jlong pt } } - auto callback = wrapper->createCallback(vibrationId); + auto callback = wrapper->createCallback(vibrationId, stepId); auto performPwleEffectFn = [&primitives, &callback](vibrator::HalWrapper* hal) { return hal->performPwleEffect(primitives, callback); }; @@ -415,7 +421,7 @@ static jlong vibratorPerformPwleEffect(JNIEnv* env, jclass /* clazz */, jlong pt } static jlong vibratorPerformPwleV2Effect(JNIEnv* env, jclass /* clazz */, jlong ptr, - jobjectArray waveform, jlong vibrationId) { + jobjectArray waveform, jlong vibrationId, jlong stepId) { VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr); if (wrapper == nullptr) { ALOGE("vibratorPerformPwleV2Effect failed because native wrapper was not initialized"); @@ -431,7 +437,7 @@ static jlong vibratorPerformPwleV2Effect(JNIEnv* env, jclass /* clazz */, jlong } composite.pwlePrimitives = primitives; - auto callback = wrapper->createCallback(vibrationId); + auto callback = wrapper->createCallback(vibrationId, stepId); auto composePwleV2Fn = [&composite, &callback](vibrator::HalWrapper* hal) { return hal->composePwleV2(composite, callback); }; @@ -610,16 +616,16 @@ static const JNINativeMethod method_table[] = { (void*)vibratorNativeInit}, {"getNativeFinalizer", "()J", (void*)vibratorGetNativeFinalizer}, {"isAvailable", "(J)Z", (void*)vibratorIsAvailable}, - {"on", "(JJJ)J", (void*)vibratorOn}, + {"on", "(JJJJ)J", (void*)vibratorOn}, {"off", "(J)V", (void*)vibratorOff}, {"setAmplitude", "(JF)V", (void*)vibratorSetAmplitude}, - {"performEffect", "(JJJJ)J", (void*)vibratorPerformEffect}, - {"performVendorEffect", "(JLandroid/os/Parcel;JFFJ)J", (void*)vibratorPerformVendorEffect}, - {"performComposedEffect", "(J[Landroid/os/vibrator/PrimitiveSegment;J)J", + {"performEffect", "(JJJJJ)J", (void*)vibratorPerformEffect}, + {"performVendorEffect", "(JLandroid/os/Parcel;JFFJJ)J", (void*)vibratorPerformVendorEffect}, + {"performComposedEffect", "(J[Landroid/os/vibrator/PrimitiveSegment;JJ)J", (void*)vibratorPerformComposedEffect}, - {"performPwleEffect", "(J[Landroid/os/vibrator/RampSegment;IJ)J", + {"performPwleEffect", "(J[Landroid/os/vibrator/RampSegment;IJJ)J", (void*)vibratorPerformPwleEffect}, - {"performPwleV2Effect", "(J[Landroid/os/vibrator/PwlePoint;J)J", + {"performPwleV2Effect", "(J[Landroid/os/vibrator/PwlePoint;JJ)J", (void*)vibratorPerformPwleV2Effect}, {"setExternalControl", "(JZ)V", (void*)vibratorSetExternalControl}, {"alwaysOnEnable", "(JJJJ)V", (void*)vibratorAlwaysOnEnable}, @@ -632,7 +638,7 @@ int register_android_server_vibrator_VibratorController(JavaVM* jvm, JNIEnv* env auto listenerClassName = "com/android/server/vibrator/VibratorController$OnVibrationCompleteListener"; jclass listenerClass = FindClassOrDie(env, listenerClassName); - sMethodIdOnComplete = GetMethodIDOrDie(env, listenerClass, "onComplete", "(IJ)V"); + sMethodIdOnComplete = GetMethodIDOrDie(env, listenerClass, "onComplete", "(IJJ)V"); jclass primitiveClass = FindClassOrDie(env, "android/os/vibrator/PrimitiveSegment"); sPrimitiveClassInfo.id = GetFieldIDOrDie(env, primitiveClass, "mPrimitiveId", "I"); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index 5b7e7f17c454..e3b9fdb16d59 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -1454,11 +1454,12 @@ final class DevicePolicyEngine { } for (int i = 0; i < mLocalPolicies.size(); i++) { - Set<PolicyKey> localPolicies = new HashSet<>( - mLocalPolicies.get(mLocalPolicies.keyAt(i)).keySet()); + // New users are potentially added to mLocalPolicies during the loop body + // (see b/374511959). + int userId = mLocalPolicies.keyAt(i); + Set<PolicyKey> localPolicies = new HashSet<>(mLocalPolicies.get(userId).keySet()); for (PolicyKey policy : localPolicies) { - PolicyState<?> policyState = mLocalPolicies.get( - mLocalPolicies.keyAt(i)).get(policy); + PolicyState<?> policyState = mLocalPolicies.get(userId).get(policy); if (policyState.getPoliciesSetByAdmins().containsKey(admin)) { removeLocalPolicy( policyState.getPolicyDefinition(), admin, mLocalPolicies.keyAt(i)); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 191c21e661d0..aee32a0473a3 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -423,6 +423,7 @@ import android.database.ContentObserver; import android.database.Cursor; import android.graphics.Bitmap; import android.hardware.usb.UsbManager; +import android.health.connect.HealthConnectManager; import android.location.Location; import android.location.LocationManager; import android.media.AudioManager; @@ -2149,6 +2150,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); mBackgroundHandler = BackgroundThread.getHandler(); + // Add the health permission to the list of restricted permissions. + if (android.permission.flags.Flags.replaceBodySensorPermissionEnabled()) { + Set<String> healthPermissions = HealthConnectManager.getHealthPermissions(mContext); + for (String permission : healthPermissions) { + SENSOR_PERMISSIONS.add(permission); + } + } + // Needed when mHasFeature == false, because it controls the certificate warning text. mCertificateMonitor = new CertificateMonitor(this, mInjector, mBackgroundHandler); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index 2d3f7231cc5c..856466675a28 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -40,10 +40,14 @@ import android.content.res.Configuration; import android.graphics.Insets; import android.os.Build; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.server.wm.WindowManagerStateHelper; import android.util.Log; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; import android.view.WindowManagerGlobal; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.Flags; @@ -72,6 +76,7 @@ import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; +import java.util.List; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -139,10 +144,9 @@ public class InputMethodServiceTest { if (!mOriginalVerboseImeTrackerLoggingEnabled) { setVerboseImeTrackerLogging(true); } + mUiDevice.setOrientationNatural(); prepareIme(); prepareActivity(); - mUiDevice.freezeRotation(); - mUiDevice.setOrientationNatural(); // Waits for input binding ready. eventually(() -> { mInputMethodService = InputMethodServiceWrapper.getInstance(); @@ -169,6 +173,9 @@ public class InputMethodServiceTest { @After public void tearDown() throws Exception { + if (!mUiDevice.isNaturalOrientation()) { + mUiDevice.setOrientationNatural(); + } mUiDevice.unfreezeRotation(); if (!mOriginalVerboseImeTrackerLoggingEnabled) { setVerboseImeTrackerLogging(false); @@ -245,6 +252,61 @@ public class InputMethodServiceTest { } /** + * This checks that the surface is removed after the window was hidden in + * InputMethodService#hideSoftInput + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) + public void testSurfaceRemovedAfterHideSoftInput() { + setShowImeWithHardKeyboard(true /* enabled */); + + // Triggers to show IME via public API. + verifyInputViewStatusOnMainSync(() -> mActivity.showImeWithWindowInsetsController(), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + + final var window = mInputMethodService.getWindow().getWindow(); + assertWithMessage("IME window exists").that(window).isNotNull(); + assertWithMessage("IME window showing").that( + window.getDecorView().getVisibility()).isEqualTo(View.VISIBLE); + + mActivity.getWindow().getDecorView().setWindowInsetsAnimationCallback( + new WindowInsetsAnimation.Callback( + WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) { + @NonNull + @Override + public WindowInsetsAnimation.Bounds onStart( + @NonNull WindowInsetsAnimation animation, + @NonNull WindowInsetsAnimation.Bounds bounds) { + return super.onStart(animation, bounds); + } + + @NonNull + @Override + public WindowInsets onProgress(@NonNull WindowInsets insets, + @NonNull List<WindowInsetsAnimation> runningAnimations) { + assertWithMessage("IME surface not removed during the animation").that( + window.getDecorView().getVisibility()).isEqualTo(View.VISIBLE); + return insets; + } + + @Override + public void onEnd(@NonNull WindowInsetsAnimation animation) { + assertWithMessage( + "IME surface not removed before the end of the animation").that( + window.getDecorView().getVisibility()).isEqualTo(View.VISIBLE); + super.onEnd(animation); + } + }); + + // Triggers to hide IME via public API. + verifyInputViewStatusOnMainSync(() -> mActivity.hideImeWithWindowInsetsController(), + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); + eventually(() -> assertWithMessage("IME window not showing").that( + window.getDecorView().getVisibility()).isEqualTo(View.GONE)); + } + + /** * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf. */ @Test @@ -866,6 +928,9 @@ public class InputMethodServiceTest { () -> mActivity.showImeWithWindowInsetsController(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + eventually(() -> assertWithMessage("IME navigation bar is shown") + .that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue()); + final var backButton = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); verifyInputViewStatus( () -> { @@ -892,6 +957,9 @@ public class InputMethodServiceTest { () -> mActivity.showImeWithWindowInsetsController(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + eventually(() -> assertWithMessage("IME navigation bar is shown") + .that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue()); + final var backButton = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); verifyInputViewStatus( () -> { @@ -927,6 +995,9 @@ public class InputMethodServiceTest { }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + eventually(() -> assertWithMessage("IME navigation bar is shown") + .that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue()); + final var imeSwitcherButton = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); imeSwitcherButton.click(); mInstrumentation.waitForIdleSync(); @@ -965,6 +1036,9 @@ public class InputMethodServiceTest { }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + eventually(() -> assertWithMessage("IME navigation bar is shown") + .that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue()); + final var imeSwitcherButton = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); imeSwitcherButton.longClick(); mInstrumentation.waitForIdleSync(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java index 7d25acd7f5e7..a42116351c2d 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java @@ -152,7 +152,7 @@ public class AutomaticBrightnessControllerTest { } @Override - AutomaticBrightnessController.Clock createClock(boolean isEnabled) { + AutomaticBrightnessController.Clock createClock() { return new AutomaticBrightnessController.Clock() { @Override public long uptimeMillis() { @@ -618,39 +618,46 @@ public class AutomaticBrightnessControllerTest { long increment = 500; // set autobrightness to low // t = 0 - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); // t = 500 mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); // t = 1000 mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(0.0f, mController.getAmbientLux(), EPSILON); // t = 1500 mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(0.0f, mController.getAmbientLux(), EPSILON); // t = 2000 // ensure that our reading is at 0. mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(0.0f, mController.getAmbientLux(), EPSILON); // t = 2500 // first 10000 lux sensor event reading mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertTrue(mController.getAmbientLux() > 0.0f); assertTrue(mController.getAmbientLux() < 10000.0f); // t = 3000 // lux reading should still not yet be 10000. mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertTrue(mController.getAmbientLux() > 0.0f); assertTrue(mController.getAmbientLux() < 10000.0f); @@ -659,45 +666,53 @@ public class AutomaticBrightnessControllerTest { // lux has been high (10000) for 1000ms. // lux reading should be 10000 // short horizon (ambient lux) is high, long horizon is still not high - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(10000.0f, mController.getAmbientLux(), EPSILON); // t = 4000 // stay high mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(10000.0f, mController.getAmbientLux(), EPSILON); // t = 4500 Mockito.clearInvocations(mBrightnessMappingStrategy); mClock.fastForward(increment); // short horizon is high, long horizon is high too - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); verify(mBrightnessMappingStrategy, times(1)).getBrightness(10000, null, -1); assertEquals(10000.0f, mController.getAmbientLux(), EPSILON); // t = 5000 mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertTrue(mController.getAmbientLux() > 0.0f); assertTrue(mController.getAmbientLux() < 10000.0f); // t = 5500 mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertTrue(mController.getAmbientLux() > 0.0f); assertTrue(mController.getAmbientLux() < 10000.0f); // t = 6000 mClock.fastForward(increment); // ambient lux goes to 0 - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(0.0f, mController.getAmbientLux(), EPSILON); // only the values within the horizon should be kept assertArrayEquals(new float[] {10000, 10000, 0, 0, 0}, mController.getLastSensorValues(), EPSILON); - assertArrayEquals(new long[] {4000, 4500, 5000, 5500, 6000}, + assertArrayEquals(new long[]{4000 + ANDROID_SLEEP_TIME, 4500 + ANDROID_SLEEP_TIME, + 5000 + ANDROID_SLEEP_TIME, 5500 + ANDROID_SLEEP_TIME, + 6000 + ANDROID_SLEEP_TIME}, mController.getLastSensorTimestamps()); } @@ -793,7 +808,8 @@ public class AutomaticBrightnessControllerTest { for (int i = 0; i < 1000; i++) { lux += increment; mClock.fastForward(increment); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); } int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment + 1); @@ -807,17 +823,17 @@ public class AutomaticBrightnessControllerTest { long sensorTimestamp = mClock.now(); for (int i = valuesCount - 1; i >= 1; i--) { assertEquals(lux, sensorValues[i], EPSILON); - assertEquals(sensorTimestamp, sensorTimestamps[i]); + assertEquals(sensorTimestamp + ANDROID_SLEEP_TIME, sensorTimestamps[i]); lux -= increment; sensorTimestamp -= increment; } assertEquals(lux, sensorValues[0], EPSILON); - assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]); + assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG + ANDROID_SLEEP_TIME, + sensorTimestamps[0]); } @Test public void testAmbientLuxBuffers_prunedBeyondLongHorizonExceptLatestValue() throws Exception { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); ArgumentCaptor<SensorEventListener> listenerCaptor = ArgumentCaptor.forClass(SensorEventListener.class); verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor), @@ -867,7 +883,8 @@ public class AutomaticBrightnessControllerTest { for (int i = 0; i < 20; i++) { lux += increment1; mClock.fastForward(increment1); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); } int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment1 + 1); @@ -877,7 +894,8 @@ public class AutomaticBrightnessControllerTest { for (int i = 0; i < initialCapacity - valuesCount; i++) { lux += increment2; mClock.fastForward(increment2); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); } float[] sensorValues = mController.getLastSensorValues(); @@ -890,7 +908,7 @@ public class AutomaticBrightnessControllerTest { long sensorTimestamp = mClock.now(); for (int i = initialCapacity - 1; i >= 1; i--) { assertEquals(lux, sensorValues[i], EPSILON); - assertEquals(sensorTimestamp, sensorTimestamps[i]); + assertEquals(sensorTimestamp + ANDROID_SLEEP_TIME, sensorTimestamps[i]); if (i >= valuesCount) { lux -= increment2; @@ -901,7 +919,8 @@ public class AutomaticBrightnessControllerTest { } } assertEquals(lux, sensorValues[0], EPSILON); - assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]); + assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG + ANDROID_SLEEP_TIME, + sensorTimestamps[0]); } @Test @@ -951,25 +970,29 @@ public class AutomaticBrightnessControllerTest { // t = 0 // Initial lux - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); // t = 1000 // Lux isn't steady yet mClock.fastForward(1000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); // t = 1500 // Lux isn't steady yet mClock.fastForward(500); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); // t = 2500 // Lux is steady now mClock.fastForward(1000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); } @@ -992,25 +1015,29 @@ public class AutomaticBrightnessControllerTest { // t = 0 // Initial lux - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); // t = 2000 // Lux isn't steady yet mClock.fastForward(2000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); // t = 2500 // Lux isn't steady yet mClock.fastForward(500); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); // t = 4500 // Lux is steady now mClock.fastForward(2000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); } @@ -1031,19 +1058,22 @@ public class AutomaticBrightnessControllerTest { // t = 0 // Initial lux - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); // t = 500 // Lux isn't steady yet mClock.fastForward(500); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); // t = 1500 // Lux is steady now mClock.fastForward(1000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); } @@ -1068,19 +1098,22 @@ public class AutomaticBrightnessControllerTest { // t = 0 // Initial lux - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); // t = 1000 // Lux isn't steady yet mClock.fastForward(1000); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(1200, mController.getAmbientLux(), EPSILON); // t = 2500 // Lux is steady now mClock.fastForward(1500); - listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500)); + listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500, + (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER)); assertEquals(500, mController.getAmbientLux(), EPSILON); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index e0e44252a8f3..c151732cec66 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -984,8 +984,7 @@ public class DisplayManagerServiceTest { Handler handler = displayManager.getDisplayHandler(); waitForIdleHandler(handler); - assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_BASIC_CHANGED, - EVENT_DISPLAY_REFRESH_RATE_CHANGED); + assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_BASIC_CHANGED); } /** 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 aed1f9858660..db94958f769e 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -1066,7 +1066,6 @@ public final class DisplayPowerControllerTest { com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true); mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true); - when(mDisplayManagerFlagsMock.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession); @@ -1172,7 +1171,6 @@ public final class DisplayPowerControllerTest { com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true); mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true); - when(mDisplayManagerFlagsMock.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(false); mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession); diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java index 2ebb6c2a3ce4..ef39167dbabc 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java @@ -240,7 +240,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void selectStrategyDoesNotSelectDozeStrategyWhenOffloadSessionAutoBrightnessIsEnabled() { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -378,7 +377,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void selectStrategy_selectsAutomaticStrategyWhenValid() { when(mDisplayManagerFlags.isRefactorDisplayPowerControllerEnabled()).thenReturn(true); - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -409,7 +407,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void selectStrategy_doesNotSelectAutomaticStrategyWhenStylusInUse() { when(mDisplayManagerFlags.isRefactorDisplayPowerControllerEnabled()).thenReturn(true); - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -536,7 +533,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void setAllowAutoBrightnessWhileDozing_enabledWhenConfigAndOffloadSessionAreEnabled() { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -550,7 +546,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void setAllowAutoBrightnessWhileDozing_disabledWhenOffloadSessionFlagIsDisabled() { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(false); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -564,7 +559,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void setAllowAutoBrightnessWhileDozing_disabledWhenABWhileDozingConfigIsDisabled() { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( @@ -588,7 +582,6 @@ public final class DisplayBrightnessStrategySelectorTest { @Test public void setAllowAutoBrightnessWhileDozing_EnabledWhenFlagsAreDisabled() { - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true); when(mResources.getBoolean(R.bool.config_allowAutoBrightnessWhileDozing)).thenReturn( true); mDisplayBrightnessStrategySelector = new DisplayBrightnessStrategySelector(mContext, @@ -600,11 +593,5 @@ public final class DisplayBrightnessStrategySelectorTest { mDisplayBrightnessStrategySelector .setAllowAutoBrightnessWhileDozing(mDisplayOffloadSession); assertTrue(mDisplayBrightnessStrategySelector.isAllowAutoBrightnessWhileDozing()); - - when(mDisplayManagerFlags.isDisplayOffloadEnabled()).thenReturn(true); - when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(false); - mDisplayBrightnessStrategySelector - .setAllowAutoBrightnessWhileDozing(mDisplayOffloadSession); - assertTrue(mDisplayBrightnessStrategySelector.isAllowAutoBrightnessWhileDozing()); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java index fa5847560782..4b53f1309337 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java @@ -23,18 +23,11 @@ import static com.android.server.am.ActivityManagerService.Injector; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import android.app.ActivityManagerInternal; import android.app.ActivityManagerInternal.FrozenProcessListener; import android.content.ComponentName; import android.content.Context; @@ -44,14 +37,12 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.MessageQueue; import android.os.Process; -import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; import android.text.TextUtils; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.internal.annotations.GuardedBy; import com.android.modules.utils.testing.ExtendedMockitoRule; import com.android.modules.utils.testing.TestableDeviceConfig; import com.android.server.LocalServices; @@ -68,11 +59,9 @@ import org.mockito.Mock; import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; /** @@ -164,7 +153,7 @@ public final class CachedAppOptimizerTest { app.info.uid = packageUid; // Exact value does not mater, it can be any state for which compaction is allowed. app.mState.setSetProcState(PROCESS_STATE_BOUND_FOREGROUND_SERVICE); - app.mState.setSetAdj(899); + app.mState.setSetAdj(940); app.mState.setCurAdj(940); 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 1ef758cf192e..340115a7d465 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -3337,6 +3337,108 @@ public class MockingOomAdjusterTests { followUpTimeCaptor.capture()); } + /** + * For Perceptible Tasks adjustment, this solely unit-tests OomAdjuster -> onOtherActivity() + */ + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_PERCEPTIBLE_TASKS) + public void testPerceptibleAdjustment() { + ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); + + long now = mInjector.getUptimeMillis(); + + // GIVEN: perceptible adjustment is NOT enabled (perceptible stop time is not set) + // EXPECT: zero adjustment + // TLDR: App is not set as a perceptible task and hence no oom_adj boosting. + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.initialize(app, CACHED_APP_MIN_ADJ, + false, false, PROCESS_STATE_CACHED_ACTIVITY, + SCHED_GROUP_DEFAULT, 0, 0, PROCESS_STATE_IMPORTANT_FOREGROUND); + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.onOtherActivity(-1); + assertEquals(CACHED_APP_MIN_ADJ, mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.adj); + + // GIVEN: perceptible adjustment is enabled (perceptible stop time is set) and + // elapsed time < PERCEPTIBLE_TASK_TIMEOUT + // EXPECT: adjustment to PERCEPTIBLE_MEDIUM_APP_ADJ + // TLDR: App is a perceptible task (e.g. opened from launcher) and has oom_adj boosting. + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.initialize(app, CACHED_APP_MIN_ADJ, + false, false, PROCESS_STATE_CACHED_ACTIVITY, + SCHED_GROUP_DEFAULT, 0, 0, PROCESS_STATE_IMPORTANT_FOREGROUND); + mInjector.reset(); + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.onOtherActivity(now); + assertEquals(PERCEPTIBLE_MEDIUM_APP_ADJ, + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.adj); + + // GIVEN: perceptible adjustment is enabled (perceptible stop time is set) and + // elapsed time > PERCEPTIBLE_TASK_TIMEOUT + // EXPECT: adjustment to PREVIOUS_APP_ADJ + // TLDR: App is a perceptible task (e.g. opened from launcher) and has oom_adj boosting, but + // time has elapsed and has dropped to a lower boosting of PREVIOUS_APP_ADJ + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.initialize(app, CACHED_APP_MIN_ADJ, + false, false, PROCESS_STATE_CACHED_ACTIVITY, + SCHED_GROUP_DEFAULT, 0, 0, PROCESS_STATE_IMPORTANT_FOREGROUND); + mInjector.jumpUptimeAheadTo(OomAdjuster.PERCEPTIBLE_TASK_TIMEOUT_MILLIS + 1000); + mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.onOtherActivity(0); + assertEquals(PREVIOUS_APP_ADJ, mService.mOomAdjuster.mTmpComputeOomAdjWindowCallback.adj); + } + + /** + * For Perceptible Tasks adjustment, this tests overall adjustment flow. + */ + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_PERCEPTIBLE_TASKS) + public void testUpdateOomAdjPerceptible() { + ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); + WindowProcessController wpc = app.getWindowProcessController(); + + // Set uptime to be at least the timeout time + buffer, so that we don't end up with + // negative stopTime in our test input + mInjector.jumpUptimeAheadTo(OomAdjuster.PERCEPTIBLE_TASK_TIMEOUT_MILLIS + 60L * 1000L); + long now = mInjector.getUptimeMillis(); + doReturn(true).when(wpc).hasActivities(); + + // GIVEN: perceptible adjustment is is enabled + // EXPECT: perceptible-act adjustment + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_STOPPING_FINISHING) + .when(wpc).getActivityStateFlags(); + doReturn(now).when(wpc).getPerceptibleTaskStoppedTimeMillis(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_IMPORTANT_BACKGROUND, PERCEPTIBLE_MEDIUM_APP_ADJ, + SCHED_GROUP_BACKGROUND, "perceptible-act"); + + // GIVEN: perceptible adjustment is is enabled and timeout has been reached + // EXPECT: stale-perceptible-act adjustment + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_STOPPING_FINISHING) + .when(wpc).getActivityStateFlags(); + + doReturn(now - OomAdjuster.PERCEPTIBLE_TASK_TIMEOUT_MILLIS).when( + wpc).getPerceptibleTaskStoppedTimeMillis(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_LAST_ACTIVITY, PREVIOUS_APP_ADJ, + SCHED_GROUP_BACKGROUND, "stale-perceptible-act"); + + // GIVEN: perceptible adjustment is is disabled + // EXPECT: no perceptible adjustment + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_STOPPING_FINISHING) + .when(wpc).getActivityStateFlags(); + doReturn(Long.MIN_VALUE).when(wpc).getPerceptibleTaskStoppedTimeMillis(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_CACHED_ACTIVITY, CACHED_APP_MIN_ADJ, + SCHED_GROUP_BACKGROUND, "cch-act"); + + // GIVEN: perceptible app is in foreground + // EXPECT: no perceptible adjustment + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_VISIBLE) + .when(wpc).getActivityStateFlags(); + doReturn(now).when(wpc).getPerceptibleTaskStoppedTimeMillis(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, + SCHED_GROUP_DEFAULT, "vis-activity"); + } + @SuppressWarnings("GuardedBy") @Test public void testUpdateOomAdj_DoAll_Multiple_Provider_Retention() { diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java index 904545bd3cc3..b4e845171a0b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java @@ -276,6 +276,7 @@ public class JobServiceContextTest { final int jobId = 123; mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); + mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; doReturn(jobId).when(mMockJobStatus).getJobId(); mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); @@ -296,7 +297,25 @@ public class JobServiceContextTest { mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); + verify(mMockJobStatus, never()).setAbandoned(true); + + mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); + doReturn(jobId).when(mMockJobStatus).getJobId(); + + mJobServiceContext.mVerb = JobServiceContext.VERB_BINDING; + mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); + verify(mMockJobStatus, never()).setAbandoned(true); + mJobServiceContext.mVerb = JobServiceContext.VERB_STARTING; + mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); + verify(mMockJobStatus, never()).setAbandoned(true); + + mJobServiceContext.mVerb = JobServiceContext.VERB_STOPPING; + mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); + verify(mMockJobStatus, never()).setAbandoned(true); + + mJobServiceContext.mVerb = JobServiceContext.VERB_FINISHED; + mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); verify(mMockJobStatus, never()).setAbandoned(true); } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index c6870adb8464..4e4b3df3c935 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -295,15 +295,15 @@ public class QuotaControllerTest { } private void setCharging() { - doReturn(true).when(mJobSchedulerService).isBatteryCharging(); synchronized (mQuotaController.mLock) { + doReturn(true).when(mJobSchedulerService).isBatteryCharging(); mQuotaController.onBatteryStateChangedLocked(); } } private void setDischarging() { - doReturn(false).when(mJobSchedulerService).isBatteryCharging(); synchronized (mQuotaController.mLock) { + doReturn(false).when(mJobSchedulerService).isBatteryCharging(); mQuotaController.onBatteryStateChangedLocked(); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index bc04fd94c719..49f2e9d70287 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -258,6 +258,8 @@ public class WallpaperManagerServiceTests { spyOn(mIpm); spyOn(mResources); doReturn(true).when(mResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mResources).getBoolean( + eq(R.bool.config_canInternalDisplayHostDesktops)); mService = new TestWallpaperManagerService(sContext); spyOn(mService); mService.systemReady(); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java index 8fc8c9f677a6..6be9c6d4b80c 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java @@ -48,6 +48,7 @@ import org.junit.runner.RunWith; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; @@ -121,7 +122,7 @@ public class BatteryUsageStatsProviderPerfTest { } @Test - public void getBatteryUsageStats_accumulated() { + public void getBatteryUsageStats_accumulated() throws IOException { BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder() .setMaxStatsAgeMs(0) .includePowerStateData() @@ -155,6 +156,8 @@ public class BatteryUsageStatsProviderPerfTest { // Verify that all iterations produce the same result assertThat(cpuConsumedPower).isEqualTo(expectedCpuPower); } + stats.close(); + state.resumeTiming(); } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/AggregatedPowerStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/AggregatedPowerStatsTest.java index 0e73329dcfe5..978fd1af2636 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/AggregatedPowerStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/AggregatedPowerStatsTest.java @@ -18,6 +18,7 @@ package com.android.server.power.stats.processor; import static com.google.common.truth.Truth.assertThat; +import android.annotation.SuppressLint; import android.os.BatteryConsumer; import android.os.PersistableBundle; import android.util.SparseArray; @@ -280,6 +281,7 @@ public class AggregatedPowerStatsTest { .isEqualTo(new long[]{250, 300}); } + @SuppressLint("CheckResult") private static long[] getDeviceStats( AggregatedPowerStats stats, int powerComponentId, int... states) { @@ -290,6 +292,7 @@ public class AggregatedPowerStatsTest { return out; } + @SuppressLint("CheckResult") private static long[] getStateStats( AggregatedPowerStats stats, int key, int... states) { PowerComponentAggregatedPowerStats powerComponentStats = @@ -299,6 +302,7 @@ public class AggregatedPowerStatsTest { return out; } + @SuppressLint("CheckResult") private static long[] getUidDeviceStats( AggregatedPowerStats stats, int powerComponentId, int uid, int... states) { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/AmbientDisplayPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/AmbientDisplayPowerStatsProcessorTest.java index 21e615f8c740..58e9d1e26f2c 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/AmbientDisplayPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/AmbientDisplayPowerStatsProcessorTest.java @@ -27,6 +27,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.when; +import android.annotation.SuppressLint; import android.hardware.power.stats.EnergyConsumerType; import android.os.BatteryConsumer; import android.os.Handler; @@ -166,6 +167,7 @@ public class AmbientDisplayPowerStatsProcessorTest { return stats.getPowerComponentStats(BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY); } + @SuppressLint("CheckResult") private void assertPowerEstimate( PowerComponentAggregatedPowerStats aggregatedStats, int powerState, int screenState, double expectedPowerEstimate) { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BasePowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BasePowerStatsProcessorTest.java index cca60339acf7..58784d7b4a25 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BasePowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BasePowerStatsProcessorTest.java @@ -34,6 +34,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; +import android.annotation.SuppressLint; import android.os.BatteryConsumer; import android.os.BatteryUsageStats; import android.os.Process; @@ -68,6 +69,7 @@ public class BasePowerStatsProcessorTest { .setProcessorSupplier(() -> new BasePowerStatsProcessor(() -> 4000)); } + @SuppressLint("CheckResult") @Test public void processPowerStats() { AggregatedPowerStats aggregatedPowerStats = prepareAggregatedPowerStats(true); @@ -95,9 +97,11 @@ public class BasePowerStatsProcessorTest { stats.getUidStats(uidStats, APP_UID1, states(POWER_STATE_BATTERY, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE)); assertThat(statsLayout.getUidUsageDuration(uidStats)).isEqualTo(5000); - stats.getUidStats(uidStats, APP_UID1, + boolean nonZero = stats.getUidStats(uidStats, APP_UID1, states(POWER_STATE_BATTERY, SCREEN_STATE_OTHER, PROCESS_STATE_UNSPECIFIED)); - assertThat(statsLayout.getUidUsageDuration(uidStats)).isEqualTo(0); + if (nonZero) { + assertThat(statsLayout.getUidUsageDuration(uidStats)).isEqualTo(0); + } stats.getUidStats(uidStats, APP_UID2, states(POWER_STATE_BATTERY, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); @@ -105,11 +109,14 @@ public class BasePowerStatsProcessorTest { stats.getUidStats(uidStats, APP_UID2, states(POWER_STATE_BATTERY, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED)); assertThat(statsLayout.getUidUsageDuration(uidStats)).isEqualTo(8500); - stats.getUidStats(uidStats, APP_UID2, + nonZero = stats.getUidStats(uidStats, APP_UID2, states(POWER_STATE_BATTERY, SCREEN_STATE_OTHER, PROCESS_STATE_UNSPECIFIED)); - assertThat(statsLayout.getUidUsageDuration(uidStats)).isEqualTo(0); + if (nonZero) { + assertThat(statsLayout.getUidUsageDuration(uidStats)).isEqualTo(0); + } } + @SuppressLint("CheckResult") @Test public void fuelgaugeAvailable() { AggregatedPowerStats aggregatedPowerStats = prepareAggregatedPowerStats(true); @@ -138,6 +145,7 @@ public class BasePowerStatsProcessorTest { assertThat(dischargeDuration).isWithin(5).of(6000); } + @SuppressLint("CheckResult") @Test public void fuelgaugeUnavailable() { AggregatedPowerStats aggregatedPowerStats = prepareAggregatedPowerStats(false); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessorTest.java index 2ff7dde3255f..e6e7f6e105b7 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BinaryStatePowerStatsProcessorTest.java @@ -30,6 +30,7 @@ import static com.android.server.power.stats.processor.AggregatedPowerStatsConfi import static com.google.common.truth.Truth.assertThat; +import android.annotation.SuppressLint; import android.os.BatteryConsumer; import android.os.BatteryStats; import android.os.PersistableBundle; @@ -74,6 +75,7 @@ public class BinaryStatePowerStatsProcessorTest { } } + @SuppressLint("CheckResult") @Test public void powerProfileModel() { BinaryStatePowerStatsLayout statsLayout = new BinaryStatePowerStatsLayout(); @@ -138,12 +140,14 @@ public class BinaryStatePowerStatsProcessorTest { assertThat(statsLayout.getUidPowerEstimate(uidStats)) .isWithin(PRECISION).of(expectedPower2); - stats.getUidStats(uidStats, APP_UID2, - states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); - assertThat(statsLayout.getUidPowerEstimate(uidStats)) - .isWithin(PRECISION).of(0); + if (stats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED))) { + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0); + } } + @SuppressLint("CheckResult") @Test public void energyConsumerModel() { BinaryStatePowerStatsLayout @@ -232,10 +236,11 @@ public class BinaryStatePowerStatsProcessorTest { assertThat(statsLayout.getUidPowerEstimate(uidStats)) .isWithin(PRECISION).of(expectedPower2); - stats.getUidStats(uidStats, APP_UID2, - states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); - assertThat(statsLayout.getUidPowerEstimate(uidStats)) - .isWithin(PRECISION).of(0); + if (stats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED))) { + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0); + } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BluetoothPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BluetoothPowerStatsProcessorTest.java index 60131861ce89..6d7119dc1f0e 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BluetoothPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/BluetoothPowerStatsProcessorTest.java @@ -33,6 +33,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.annotation.SuppressLint; import android.bluetooth.BluetoothActivityEnergyInfo; import android.bluetooth.BluetoothAdapter; import android.bluetooth.UidTraffic; @@ -161,6 +162,7 @@ public class BluetoothPowerStatsProcessorTest { when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)).thenReturn(true); } + @SuppressLint("CheckResult") @Test public void powerProfileModel_mostlyDataTransfer() { // No power monitoring hardware @@ -262,6 +264,7 @@ public class BluetoothPowerStatsProcessorTest { .isWithin(PRECISION).of(expectedPower2 * 0.75); } + @SuppressLint("CheckResult") @Test public void powerProfileModel_mostlyScan() { // No power monitoring hardware @@ -361,6 +364,7 @@ public class BluetoothPowerStatsProcessorTest { .isWithin(PRECISION).of(expectedPower2 * 0.75); } + @SuppressLint("CheckResult") @Test public void consumedEnergyModel() { when(mConsumedEnergyRetriever.getVoltageMv()).thenReturn(VOLTAGE_MV); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CameraPowerStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CameraPowerStatsTest.java index 23642de466a1..a95963242d8f 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CameraPowerStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CameraPowerStatsTest.java @@ -33,6 +33,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; +import android.annotation.SuppressLint; import android.hardware.power.stats.EnergyConsumerResult; import android.hardware.power.stats.EnergyConsumerType; import android.os.BatteryConsumer; @@ -117,6 +118,7 @@ public class CameraPowerStatsTest { mMonotonicClock = new MonotonicClock(0, mStatsRule.getMockClock()); } + @SuppressLint("CheckResult") @Test public void energyConsumerModel() { when(mConsumedEnergyRetriever.getVoltageMv()).thenReturn(VOLTAGE_MV); @@ -211,10 +213,11 @@ public class CameraPowerStatsTest { assertThat(statsLayout.getUidPowerEstimate(uidStats)) .isWithin(PRECISION).of(expectedPower2); - stats.getUidStats(uidStats, APP_UID2, - states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); - assertThat(statsLayout.getUidPowerEstimate(uidStats)) - .isWithin(PRECISION).of(0); + if (stats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED))) { + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0); + } } private BatteryStats.HistoryItem buildHistoryItem(int timestamp, boolean stateOn, diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CpuPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CpuPowerStatsProcessorTest.java index cb9d9b12a2fc..dcddf06f01fb 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CpuPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CpuPowerStatsProcessorTest.java @@ -34,6 +34,7 @@ import static org.junit.Assert.fail; import android.os.BatteryConsumer; import android.os.PersistableBundle; import android.platform.test.ravenwood.RavenwoodRule; +import android.util.IntArray; import android.util.LongArray; import androidx.test.filters.SmallTest; @@ -51,7 +52,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -315,8 +315,12 @@ public class CpuPowerStatsProcessorTest { } @Override - void collectUids(Collection<Integer> uids) { - uids.addAll(mUids); + IntArray getActiveUids() { + IntArray uids = new IntArray(); + for (Integer uid : mUids) { + uids.add(uid); + } + return uids; } void verifyPowerEstimates() { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsTest.java index 42baba765276..a421675f1896 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/CustomEnergyConsumerPowerStatsTest.java @@ -33,6 +33,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.when; +import android.annotation.SuppressLint; import android.hardware.power.stats.EnergyConsumerAttribution; import android.hardware.power.stats.EnergyConsumerResult; import android.hardware.power.stats.EnergyConsumerType; @@ -154,6 +155,7 @@ public class CustomEnergyConsumerPowerStatsTest { .isEqualTo(6000); } + @SuppressLint("CheckResult") @Test public void processStats() throws Exception { AggregatedPowerStats aggregatedPowerStats = createAggregatedPowerStats(); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/GnssPowerStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/GnssPowerStatsTest.java index c63267ec6ae7..b4f21133f621 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/GnssPowerStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/GnssPowerStatsTest.java @@ -33,6 +33,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; +import android.annotation.SuppressLint; import android.hardware.power.stats.EnergyConsumerResult; import android.hardware.power.stats.EnergyConsumerType; import android.location.GnssSignalQuality; @@ -122,6 +123,7 @@ public class GnssPowerStatsTest { mHistoryItem.clear(); } + @SuppressLint("CheckResult") @Test public void powerProfileModel() { // ODPM unsupported @@ -206,12 +208,14 @@ public class GnssPowerStatsTest { assertThat(statsLayout.getUidPowerEstimate(uidStats)) .isWithin(PRECISION).of(0.51111); - stats.getUidStats(uidStats, APP_UID2, - states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); - assertThat(statsLayout.getUidPowerEstimate(uidStats)) - .isWithin(PRECISION).of(0); + if (stats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED))) { + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0); + } } + @SuppressLint("CheckResult") @Test public void initialStateGnssOn() { // ODPM unsupported @@ -285,12 +289,14 @@ public class GnssPowerStatsTest { assertThat(statsLayout.getUidPowerEstimate(uidStats)) .isWithin(PRECISION).of(0.51111); - stats.getUidStats(uidStats, APP_UID2, - states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); - assertThat(statsLayout.getUidPowerEstimate(uidStats)) - .isWithin(PRECISION).of(0); + if (stats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED))) { + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0); + } } + @SuppressLint("CheckResult") @Test public void energyConsumerModel() { when(mConsumedEnergyRetriever.getVoltageMv()).thenReturn(VOLTAGE_MV); @@ -386,10 +392,11 @@ public class GnssPowerStatsTest { assertThat(statsLayout.getUidPowerEstimate(uidStats)) .isWithin(PRECISION).of(expectedPower2); - stats.getUidStats(uidStats, APP_UID2, - states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); - assertThat(statsLayout.getUidPowerEstimate(uidStats)) - .isWithin(PRECISION).of(0); + if (stats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED))) { + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0); + } } private BatteryStats.HistoryItem buildHistoryItemInitialStateGpsOn(long timestamp) { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java index 6acd36885b1e..3dc401769e7d 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; import android.hardware.power.stats.EnergyConsumerResult; @@ -170,6 +171,7 @@ public class MobileRadioPowerStatsProcessorTest { .thenAnswer(invocation -> invocation.getArgument(0)); } + @SuppressLint("CheckResult") @Test public void powerProfileModel() { // No power monitoring hardware diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MultiStateStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MultiStateStatsTest.java index a232c0c7aec9..9abb06f338e3 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MultiStateStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MultiStateStatsTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; +import android.annotation.SuppressLint; import android.os.BatteryConsumer; import androidx.test.filters.SmallTest; @@ -124,6 +125,7 @@ public class MultiStateStatsTest { assertThat(e.getMessage()).contains("40"); } + @SuppressLint("CheckResult") @Test public void multiStateStats_aggregation() { MultiStateStats.Factory factory = makeFactory(true, true, false); @@ -143,6 +145,8 @@ public class MultiStateStatsTest { multiStateStats.increment(new long[]{200, 200}, 5000); + multiStateStats.increment(null, 6000); // No-op + long[] stats = new long[DIMENSION_COUNT]; multiStateStats.getStats(stats, new int[]{0, BatteryConsumer.PROCESS_STATE_FOREGROUND, 0}); // (400 - 100) * 0.5 + (600 - 400) * 0.5 @@ -157,9 +161,9 @@ public class MultiStateStatsTest { // (400 - 100) * 0 + (600 - 400) * 0.5 assertThat(stats).isEqualTo(new long[]{100, 100}); - multiStateStats.getStats(stats, new int[]{1, BatteryConsumer.PROCESS_STATE_BACKGROUND, 0}); // Never been in this composite state - assertThat(stats).isEqualTo(new long[]{0, 0}); + assertThat(multiStateStats.getStats(stats, + new int[]{1, BatteryConsumer.PROCESS_STATE_BACKGROUND, 0})).isFalse(); } @Test diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java index a20274fb5ded..2f742d74d8e6 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java @@ -29,6 +29,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; +import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; import android.hardware.power.stats.EnergyConsumerType; @@ -159,6 +160,7 @@ public class PhoneCallPowerStatsProcessorTest { mStatsRule.setTestPowerProfile("power_profile_test_legacy_modem"); } + @SuppressLint("CheckResult") @Test public void copyEstimatesFromMobileRadioPowerStats() { AggregatedPowerStatsConfig config = new AggregatedPowerStatsConfig(); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/ScreenPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/ScreenPowerStatsProcessorTest.java index 185216583f1b..31456a1574d0 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/ScreenPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/ScreenPowerStatsProcessorTest.java @@ -29,6 +29,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; +import android.annotation.SuppressLint; import android.hardware.power.stats.EnergyConsumerResult; import android.hardware.power.stats.EnergyConsumerType; import android.os.BatteryConsumer; @@ -278,6 +279,7 @@ public class ScreenPowerStatsProcessorTest { .of(expectedDozePowerEstimate); } + @SuppressLint("CheckResult") private void assertUidPowerEstimate( PowerComponentAggregatedPowerStats aggregatedStats, int uid, int powerState, int screenState, double expectedScreenPowerEstimate) { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/SensorPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/SensorPowerStatsProcessorTest.java index d97260455bdd..c2f01d1fa65c 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/SensorPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/SensorPowerStatsProcessorTest.java @@ -33,6 +33,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.annotation.SuppressLint; import android.hardware.Sensor; import android.hardware.SensorManager; import android.hardware.input.InputSensorInfo; @@ -91,6 +92,7 @@ public class SensorPowerStatsProcessorTest { List.of(sensor1, sensor2, sensor3)); } + @SuppressLint("CheckResult") @Test public void testPowerEstimation() { PowerComponentAggregatedPowerStats stats = createAggregatedPowerStats( diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java index 8257d714a5d5..5ac7216194a4 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java @@ -31,6 +31,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; +import android.annotation.SuppressLint; import android.os.BatteryConsumer; import android.os.PersistableBundle; import android.os.Process; @@ -123,6 +124,7 @@ public class WakelockPowerStatsProcessorTest { return history; } + @SuppressLint("CheckResult") private void assertAggregatedPowerStats(AggregatedPowerStats aggregatedPowerStats) { PowerComponentAggregatedPowerStats stats = aggregatedPowerStats.getPowerComponentStats(POWER_COMPONENT_WAKELOCK); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java index bd92a84fc815..e36056a98c85 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; import android.hardware.power.stats.EnergyConsumerResult; @@ -198,6 +199,7 @@ public class WifiPowerStatsProcessorTest { mBatteryStats = mStatsRule.getBatteryStats(); } + @SuppressLint("CheckResult") @Test public void powerProfileModel_powerController() { when(mWifiManager.isEnhancedPowerReportingSupported()).thenReturn(true); @@ -310,6 +312,7 @@ public class WifiPowerStatsProcessorTest { .isWithin(PRECISION).of(expectedPower2 * 0.75); } + @SuppressLint("CheckResult") @Test public void consumedEnergyModel_powerController() { when(mWifiManager.isEnhancedPowerReportingSupported()).thenReturn(true); @@ -424,6 +427,7 @@ public class WifiPowerStatsProcessorTest { .isWithin(PRECISION).of(expectedPower2 * 0.75); } + @SuppressLint("CheckResult") @Test public void powerProfileModel_noPowerController() { when(mWifiManager.isEnhancedPowerReportingSupported()).thenReturn(false); diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 834fea46e505..4531b3948495 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -199,6 +199,10 @@ <service android:name="com.android.server.job.MockBiasJobService" android:permission="android.permission.BIND_JOB_SERVICE"/> + <activity + android:name="android.app.Activity" + android:exported="false" /> + <activity android:name="com.android.server.pm.BaseShortcutManagerTest$ShortcutActivity"/> <activity android:name="com.android.server.pm.BaseShortcutManagerTest$ShortcutActivity2"/> <activity android:name="com.android.server.pm.BaseShortcutManagerTest$ShortcutActivity3"/> diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java index 8dfd54fe38bc..42b84bdc51e6 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java @@ -46,12 +46,11 @@ import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityActi import static android.view.accessibility.AccessibilityNodeInfo.FOCUS_INPUT; import static android.view.accessibility.AccessibilityNodeInfo.ROOT_NODE_ID; -import static org.hamcrest.Matchers.hasItems; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -65,6 +64,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.accessibilityservice.AccessibilityService; @@ -90,14 +90,20 @@ import android.os.Process; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.test.FakePermissionEnforcer; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; import android.view.Display; import android.view.KeyEvent; import android.view.MagnificationSpec; +import android.view.SurfaceControl; +import android.view.WindowManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.view.accessibility.IWindowSurfaceInfoCallback; +import android.window.ScreenCapture; import com.android.server.accessibility.AccessibilityWindowManager.RemoteAccessibilityConnection; import com.android.server.accessibility.magnification.MagnificationProcessor; @@ -105,6 +111,7 @@ import com.android.server.accessibility.test.MessageCapturingHandler; import com.android.server.wm.WindowManagerInternal; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -121,6 +128,10 @@ import java.util.concurrent.Callable; * Tests for the AbstractAccessibilityServiceConnection */ public class AbstractAccessibilityServiceConnectionTest { + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final ComponentName COMPONENT_NAME = new ComponentName( "com.android.server.accessibility", ".AbstractAccessibilityServiceConnectionTest"); private static final String PACKAGE_NAME1 = "com.android.server.accessibility1"; @@ -264,7 +275,7 @@ public class AbstractAccessibilityServiceConnectionTest { @Test public void getCapabilities() { - assertThat(mServiceConnection.getCapabilities(), is(A11Y_SERVICE_CAPABILITY)); + assertThat(mServiceConnection.getCapabilities()).isEqualTo(A11Y_SERVICE_CAPABILITY); } @Test @@ -329,7 +340,7 @@ public class AbstractAccessibilityServiceConnectionTest { 0, null, 0); mServiceConnection.setServiceInfo(serviceInfo); - assertThat(mServiceConnection.canReceiveEventsLocked(), is(true)); + assertThat(mServiceConnection.canReceiveEventsLocked()).isTrue(); } @Test @@ -348,9 +359,11 @@ public class AbstractAccessibilityServiceConnectionTest { mServiceConnection.getWindows(); assertEquals(2, allWindows.size()); - assertThat(allWindows.get(Display.DEFAULT_DISPLAY), is(mA11yWindowInfos)); + assertThat(allWindows.get(Display.DEFAULT_DISPLAY)) + .containsExactlyElementsIn(mA11yWindowInfos); assertEquals(2, allWindows.get(Display.DEFAULT_DISPLAY).size()); - assertThat(allWindows.get(SECONDARY_DISPLAY_ID), is(mA11yWindowInfosOnSecondDisplay)); + assertThat(allWindows.get(SECONDARY_DISPLAY_ID)) + .containsExactlyElementsIn(mA11yWindowInfosOnSecondDisplay); assertEquals(1, allWindows.get(SECONDARY_DISPLAY_ID).size()); } @@ -358,12 +371,12 @@ public class AbstractAccessibilityServiceConnectionTest { public void getWindows_returnNull() { // no canRetrieveWindows, should return null when(mMockSecurityPolicy.canRetrieveWindowsLocked(mServiceConnection)).thenReturn(false); - assertThat(mServiceConnection.getWindows(), is(nullValue())); + assertThat(mServiceConnection.getWindows()).isNull(); // no checkAccessibilityAccess, should return null when(mMockSecurityPolicy.canRetrieveWindowsLocked(mServiceConnection)).thenReturn(true); when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(false); - assertThat(mServiceConnection.getWindows(), is(nullValue())); + assertThat(mServiceConnection.getWindows()).isNull(); } @Test @@ -378,19 +391,19 @@ public class AbstractAccessibilityServiceConnectionTest { @Test public void getWindow() { - assertThat(mServiceConnection.getWindow(WINDOWID), is(mA11yWindowInfos.get(0))); + assertThat(mServiceConnection.getWindow(WINDOWID)).isEqualTo(mA11yWindowInfos.get(0)); } @Test public void getWindow_returnNull() { // no canRetrieveWindows, should return null when(mMockSecurityPolicy.canRetrieveWindowsLocked(mServiceConnection)).thenReturn(false); - assertThat(mServiceConnection.getWindow(WINDOWID), is(nullValue())); + assertThat(mServiceConnection.getWindow(WINDOWID)).isNull(); // no checkAccessibilityAccess, should return null when(mMockSecurityPolicy.canRetrieveWindowsLocked(mServiceConnection)).thenReturn(true); when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(false); - assertThat(mServiceConnection.getWindow(WINDOWID), is(nullValue())); + assertThat(mServiceConnection.getWindow(WINDOWID)).isNull(); } @Test @@ -405,8 +418,8 @@ public class AbstractAccessibilityServiceConnectionTest { @Test public void getWindow_onNonDefaultDisplay() { - assertThat(mServiceConnection.getWindow(WINDOWID_ONSECONDDISPLAY), - is(mA11yWindowInfosOnSecondDisplay.get(0))); + assertThat(mServiceConnection.getWindow(WINDOWID_ONSECONDDISPLAY)) + .isEqualTo(mA11yWindowInfosOnSecondDisplay.get(0)); } @Test @@ -415,9 +428,9 @@ public class AbstractAccessibilityServiceConnectionTest { when(mMockSecurityPolicy.canGetAccessibilityNodeInfoLocked( USER_ID, mServiceConnection, WINDOWID)).thenReturn(false); for (int i = 0; i < mFindA11yNodesFunctions.length; i++) { - assertThat(mFindA11yNodesFunctions[i].call(), is(nullValue())); + assertThat(mFindA11yNodesFunctions[i].call()).isNull(); } - assertThat(mPerformA11yAction.call(), is(false)); + assertThat(mPerformA11yAction.call()).isFalse(); verifyNoMoreInteractions(mMockIA11yInteractionConnection); verify(mMockSecurityPolicy, never()).computeValidReportedPackages(any(), anyInt()); @@ -428,9 +441,9 @@ public class AbstractAccessibilityServiceConnectionTest { throws Exception { when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(false); for (int i = 0; i < mFindA11yNodesFunctions.length; i++) { - assertThat(mFindA11yNodesFunctions[i].call(), is(nullValue())); + assertThat(mFindA11yNodesFunctions[i].call()).isNull(); } - assertThat(mPerformA11yAction.call(), is(false)); + assertThat(mPerformA11yAction.call()).isFalse(); verifyNoMoreInteractions(mMockIA11yInteractionConnection); verify(mMockSecurityPolicy, never()).computeValidReportedPackages(any(), anyInt()); @@ -441,9 +454,9 @@ public class AbstractAccessibilityServiceConnectionTest { throws Exception { when(mMockA11yWindowManager.getConnectionLocked(USER_ID, WINDOWID)).thenReturn(null); for (int i = 0; i < mFindA11yNodesFunctions.length; i++) { - assertThat(mFindA11yNodesFunctions[i].call(), is(nullValue())); + assertThat(mFindA11yNodesFunctions[i].call()).isNull(); } - assertThat(mPerformA11yAction.call(), is(false)); + assertThat(mPerformA11yAction.call()).isFalse(); verifyNoMoreInteractions(mMockIA11yInteractionConnection); verify(mMockSecurityPolicy, never()).computeValidReportedPackages(any(), anyInt()); @@ -562,7 +575,7 @@ public class AbstractAccessibilityServiceConnectionTest { when(mMockFingerprintGestureDispatcher.isFingerprintGestureDetectionAvailable()) .thenReturn(true); final boolean result = mServiceConnection.isFingerprintGestureDetectionAvailable(); - assertThat(result, is(true)); + assertThat(result).isTrue(); } @Test @@ -573,14 +586,14 @@ public class AbstractAccessibilityServiceConnectionTest { // Return false if device does not support fingerprint when(mMockPackageManager.hasSystemFeature(FEATURE_FINGERPRINT)).thenReturn(false); boolean result = mServiceConnection.isFingerprintGestureDetectionAvailable(); - assertThat(result, is(false)); + assertThat(result).isFalse(); // Return false if service does not have flag when(mMockPackageManager.hasSystemFeature(FEATURE_FINGERPRINT)).thenReturn(true); mSpyServiceInfo.flags = A11Y_SERVICE_FLAG & ~FLAG_REQUEST_FINGERPRINT_GESTURES; mServiceConnection.setServiceInfo(mSpyServiceInfo); result = mServiceConnection.isFingerprintGestureDetectionAvailable(); - assertThat(result, is(false)); + assertThat(result).isFalse(); } @Test @@ -590,7 +603,7 @@ public class AbstractAccessibilityServiceConnectionTest { when(mMockMagnificationProcessor.getScale(displayId)).thenReturn(scale); final float result = mServiceConnection.getMagnificationScale(displayId); - assertThat(result, is(scale)); + assertThat(result).isEqualTo(scale); } @Test @@ -601,7 +614,7 @@ public class AbstractAccessibilityServiceConnectionTest { when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2); final float result = mServiceConnection.getMagnificationScale(displayId); - assertThat(result, is(1.0f)); + assertThat(result).isEqualTo(1.0f); } @Test @@ -616,7 +629,7 @@ public class AbstractAccessibilityServiceConnectionTest { when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2); final Region result = mServiceConnection.getMagnificationRegion(displayId); - assertThat(result.isEmpty(), is(true)); + assertWithMessage("Non-empty region: " + result).that(result.isEmpty()).isTrue(); } @Test @@ -642,7 +655,7 @@ public class AbstractAccessibilityServiceConnectionTest { when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2); final float result = mServiceConnection.getMagnificationCenterX(displayId); - assertThat(result, is(0.0f)); + assertThat(result).isEqualTo(0.0f); } @Test @@ -654,7 +667,7 @@ public class AbstractAccessibilityServiceConnectionTest { when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2); final float result = mServiceConnection.getMagnificationCenterY(displayId); - assertThat(result, is(0.0f)); + assertThat(result).isEqualTo(0.0f); } @Test @@ -664,7 +677,7 @@ public class AbstractAccessibilityServiceConnectionTest { true); final boolean result = mServiceConnection.resetMagnification(displayId, true); - assertThat(result, is(true)); + assertThat(result).isTrue(); } @Test @@ -675,7 +688,7 @@ public class AbstractAccessibilityServiceConnectionTest { when(mMockSecurityPolicy.canControlMagnification(mServiceConnection)).thenReturn(false); final boolean result = mServiceConnection.resetMagnification(displayId, true); - assertThat(result, is(false)); + assertThat(result).isFalse(); } @Test @@ -686,7 +699,7 @@ public class AbstractAccessibilityServiceConnectionTest { when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2); final boolean result = mServiceConnection.resetMagnification(displayId, true); - assertThat(result, is(false)); + assertThat(result).isFalse(); } @Test @@ -765,6 +778,134 @@ public class AbstractAccessibilityServiceConnectionTest { == bundle.getInt(KEY_ACCESSIBILITY_SCREENSHOT_STATUS))); } + private void setPreinstalledA11yTool(boolean isPreinstalledA11yTool) { + when(mSpyServiceInfo.getResolveInfo().serviceInfo.applicationInfo.isSystemApp()) + .thenReturn(isPreinstalledA11yTool); + when(mSpyServiceInfo.isAccessibilityTool()).thenReturn(isPreinstalledA11yTool); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_SECURE_SCREENSHOTS) + public void takeScreenshot_standardService_cannotCaptureSecureLayers() { + setPreinstalledA11yTool(false); + + takeScreenshotOfDisplay(); + + final ArgumentCaptor<ScreenCapture.CaptureArgs> displayArgsCaptor = + ArgumentCaptor.forClass(ScreenCapture.CaptureArgs.class); + verify(mMockWindowManagerInternal).captureDisplay( + eq(Display.DEFAULT_DISPLAY), displayArgsCaptor.capture(), any()); + assertThat(displayArgsCaptor.getValue().mCaptureSecureLayers).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_SECURE_SCREENSHOTS) + public void takeScreenshot_preinstalledA11yTool_canCaptureSecureLayers() { + setPreinstalledA11yTool(true); + + takeScreenshotOfDisplay(); + + final ArgumentCaptor<ScreenCapture.CaptureArgs> displayArgsCaptor = + ArgumentCaptor.forClass(ScreenCapture.CaptureArgs.class); + verify(mMockWindowManagerInternal).captureDisplay( + anyInt(), displayArgsCaptor.capture(), any()); + assertThat(displayArgsCaptor.getValue().mCaptureSecureLayers).isTrue(); + } + + private void takeScreenshotOfDisplay() { + when(mMockSecurityPolicy.canTakeScreenshotLocked(mServiceConnection)).thenReturn(true); + when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(true); + + final DisplayManager displayManager = new DisplayManager(mMockContext); + when(mMockContext.getSystemService(Context.DISPLAY_SERVICE)).thenReturn(displayManager); + + mServiceConnection.takeScreenshot(Display.DEFAULT_DISPLAY, + new RemoteCallback(mMockListener)); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_SECURE_SCREENSHOTS) + public void takeScreenshotOfWindow_standardWindow_standardService_cannotCaptureSecureLayers() + throws Exception { + setPreinstalledA11yTool(false); + + takeScreenshotOfWindow(/*windowFlags=*/0); + + // Screenshot was allowed + final ArgumentCaptor<ScreenCapture.LayerCaptureArgs> layerArgsCaptor = + ArgumentCaptor.forClass(ScreenCapture.LayerCaptureArgs.class); + verify(mMockSystemSupport).performScreenCapture(layerArgsCaptor.capture(), any()); + // ...without secure layers included + assertThat(layerArgsCaptor.getValue().mCaptureSecureLayers).isFalse(); + // No error sent to callback + verifyZeroInteractions(mMockCallback); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_SECURE_SCREENSHOTS) + public void takeScreenshotOfWindow_standardWindow_preinstalledA11yTool_canCaptureSecureLayers() + throws Exception { + setPreinstalledA11yTool(true); + + takeScreenshotOfWindow(/*windowFlags=*/0); + + // Screenshot was allowed + final ArgumentCaptor<ScreenCapture.LayerCaptureArgs> layerArgsCaptor = + ArgumentCaptor.forClass(ScreenCapture.LayerCaptureArgs.class); + verify(mMockSystemSupport).performScreenCapture(layerArgsCaptor.capture(), any()); + // ...with secure layers included + assertThat(layerArgsCaptor.getValue().mCaptureSecureLayers).isTrue(); + // No error sent to callback + verifyZeroInteractions(mMockCallback); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_SECURE_SCREENSHOTS) + public void takeScreenshotOfWindow_secureWindow_standardService_sendsCallbackError() + throws Exception { + setPreinstalledA11yTool(false); + + takeScreenshotOfWindow(WindowManager.LayoutParams.FLAG_SECURE); + + // Screenshot was not allowed + verify(mMockSystemSupport, never()).performScreenCapture(any(), any()); + // Error sent to callback + verify(mMockCallback).sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_SECURE_WINDOW, INTERACTION_ID); + } + + @Test + @EnableFlags(Flags.FLAG_ALLOW_SECURE_SCREENSHOTS) + public void takeScreenshotOfWindow_secureWindow_preinstalledA11yTool_canCaptureSecureLayers() + throws Exception { + setPreinstalledA11yTool(true); + + takeScreenshotOfWindow(WindowManager.LayoutParams.FLAG_SECURE); + + // Screenshot was allowed + final ArgumentCaptor<ScreenCapture.LayerCaptureArgs> layerArgsCaptor = + ArgumentCaptor.forClass(ScreenCapture.LayerCaptureArgs.class); + verify(mMockSystemSupport).performScreenCapture(layerArgsCaptor.capture(), any()); + // ...with secure layers included + assertThat(layerArgsCaptor.getValue().mCaptureSecureLayers).isTrue(); + // No error sent to callback + verifyZeroInteractions(mMockCallback); + } + + private void takeScreenshotOfWindow(int windowFlags) throws Exception { + when(mMockSecurityPolicy.canTakeScreenshotLocked(mServiceConnection)).thenReturn(true); + when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(true); + + mServiceConnection.takeScreenshotOfWindow( + WINDOWID, INTERACTION_ID, /*listener=*/null, mMockCallback); + final ArgumentCaptor<IWindowSurfaceInfoCallback> windowSurfaceCallbackCaptor = + ArgumentCaptor.forClass(IWindowSurfaceInfoCallback.class); + verify(mMockIA11yInteractionConnection).getWindowSurfaceInfo( + windowSurfaceCallbackCaptor.capture()); + windowSurfaceCallbackCaptor.getValue().provideWindowSurfaceInfo( + windowFlags, /*appUid=*/0, new SurfaceControl()); + } + private void updateServiceInfo(AccessibilityServiceInfo serviceInfo, int eventType, int feedbackType, int flags, String[] packageNames, int notificationTimeout) { serviceInfo.eventTypes = eventType; @@ -837,8 +978,8 @@ public class AbstractAccessibilityServiceConnectionTest { ArgumentCaptor.forClass(AccessibilityNodeInfo.class); verify(mMockCallback).setFindAccessibilityNodeInfoResult(captor.capture(), eq(INTERACTION_ID)); - assertThat(captor.getValue().getActionList(), - hasItems(AccessibilityAction.ACTION_CLICK, AccessibilityAction.ACTION_EXPAND)); + assertThat(captor.getValue().getActionList()).containsAtLeast( + AccessibilityAction.ACTION_CLICK, AccessibilityAction.ACTION_EXPAND); } private static class TestAccessibilityServiceConnection diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index 7f60caaa569b..0745c68fd337 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -25,6 +25,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.Context; import android.platform.test.annotations.DisableFlags; @@ -400,6 +401,46 @@ public class AutoclickControllerTest { .isNotEqualTo(initialScheduledTime); } + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void pauseButton_flagOn_clickNotTriggeredWhenPaused() { + injectFakeMouseActionHoverMoveEvent(); + + // Pause autoclick. + AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); + when(mockAutoclickTypePanel.isPaused()).thenReturn(true); + mController.mAutoclickTypePanel = mockAutoclickTypePanel; + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + + // Verify there is not a pending click. + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1); + + // Resume autoclick. + when(mockAutoclickTypePanel.isPaused()).thenReturn(false); + + // Send initial move event again. Because this is the first recorded move, a click won't be + // scheduled. + injectFakeMouseActionHoverMoveEvent(); + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse(); + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1); + + // Send move again to trigger click and verify there is now a pending click. + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isNotEqualTo(-1); + } + private void injectFakeMouseActionHoverMoveEvent() { MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_MOUSE); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java index b6d8e87d0780..c60c4b60d081 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java @@ -21,6 +21,9 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.CORNER_BOTTOM_LEFT; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.CORNER_BOTTOM_RIGHT; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.CORNER_TOP_RIGHT; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.ClickPanelControllerInterface; import static com.google.common.truth.Truth.assertThat; @@ -30,6 +33,8 @@ import android.graphics.drawable.GradientDrawable; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; +import android.view.Gravity; +import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.LinearLayout; @@ -64,8 +69,10 @@ public class AutoclickTypePanelTest { private LinearLayout mDragButton; private LinearLayout mScrollButton; private LinearLayout mPauseButton; + private LinearLayout mPositionButton; private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK; + private boolean mPaused; private final ClickPanelControllerInterface clickPanelController = new ClickPanelControllerInterface() { @@ -75,7 +82,9 @@ public class AutoclickTypePanelTest { } @Override - public void toggleAutoclickPause() {} + public void toggleAutoclickPause(boolean paused) { + mPaused = paused; + } }; @Before @@ -93,6 +102,7 @@ public class AutoclickTypePanelTest { mScrollButton = contentView.findViewById(R.id.accessibility_autoclick_scroll_layout); mDragButton = contentView.findViewById(R.id.accessibility_autoclick_drag_layout); mPauseButton = contentView.findViewById(R.id.accessibility_autoclick_pause_layout); + mPositionButton = contentView.findViewById(R.id.accessibility_autoclick_position_layout); } @Test @@ -175,9 +185,149 @@ public class AutoclickTypePanelTest { assertThat(mActiveClickType).isEqualTo(AUTOCLICK_TYPE_SCROLL); } + @Test + public void moveToNextCorner_positionButton_rotatesThroughAllPositions() { + // Define all positions in sequence + int[][] expectedPositions = { + {0, Gravity.END | Gravity.BOTTOM, /*x=*/ 15, /*y=*/ 90}, + {1, Gravity.START | Gravity.BOTTOM, /*x=*/ 15, /*y=*/ 90}, + {2, Gravity.START | Gravity.TOP, /*x=*/ 15, /*y=*/ 30}, + {3, Gravity.END | Gravity.TOP, /*x=*/ 15, /*y=*/ 30}, + {0, Gravity.END | Gravity.BOTTOM, /*x=*/ 15, /*y=*/ 90} + }; + + // Check initial position + verifyPanelPosition(expectedPositions[0]); + + // Move through all corners. + for (int i = 1; i < expectedPositions.length; i++) { + mPositionButton.callOnClick(); + verifyPanelPosition(expectedPositions[i]); + } + } + + @Test + public void pauseButton_onClick() { + mPauseButton.callOnClick(); + assertThat(mPaused).isTrue(); + assertThat(mAutoclickTypePanel.isPaused()).isTrue(); + + mPauseButton.callOnClick(); + assertThat(mPaused).isFalse(); + assertThat(mAutoclickTypePanel.isPaused()).isFalse(); + } + + @Test + public void onTouch_dragMove_updatesPosition() { + View contentView = mAutoclickTypePanel.getContentViewForTesting(); + WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting(); + int[] panelLocation = new int[2]; + contentView.getLocationOnScreen(panelLocation); + + // Define movement delta for both x and y directions. + int delta = 15; + + // Dispatch initial down event. + float touchX = panelLocation[0] + 10; + float touchY = panelLocation[1] + 10; + MotionEvent downEvent = MotionEvent.obtain( + 0, 0, + MotionEvent.ACTION_DOWN, touchX, touchY, 0); + contentView.dispatchTouchEvent(downEvent); + + // Create move event with delta, move from (x, y) to (x + delta, y + delta) + MotionEvent moveEvent = MotionEvent.obtain( + 0, 0, + MotionEvent.ACTION_MOVE, touchX + delta, touchY + delta, 0); + contentView.dispatchTouchEvent(moveEvent); + + // Verify position update. + assertThat(mAutoclickTypePanel.getIsDraggingForTesting()).isTrue(); + assertThat(params.gravity).isEqualTo(Gravity.LEFT | Gravity.TOP); + assertThat(params.x).isEqualTo(panelLocation[0] + delta); + assertThat(params.y).isEqualTo(panelLocation[1] + delta); + } + + @Test + public void dragAndEndAtRight_snapsToRightSide() { + View contentView = mAutoclickTypePanel.getContentViewForTesting(); + WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting(); + int[] panelLocation = new int[2]; + contentView.getLocationOnScreen(panelLocation); + + int screenWidth = mTestableContext.getResources().getDisplayMetrics().widthPixels; + + // Verify initial corner is bottom-right. + assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()) + .isEqualTo(CORNER_BOTTOM_RIGHT); + + dispatchDragSequence(contentView, + /* startX =*/ panelLocation[0] + 10, /* startY =*/ panelLocation[1] + 10, + /* endX =*/ (float) (screenWidth * 3) / 4, /* endY =*/ panelLocation[1] + 10); + + // Verify snapping to the right. + assertThat(params.gravity).isEqualTo(Gravity.END | Gravity.TOP); + assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()) + .isEqualTo(CORNER_TOP_RIGHT); + } + + @Test + public void dragAndEndAtLeft_snapsToLeftSide() { + View contentView = mAutoclickTypePanel.getContentViewForTesting(); + WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting(); + int[] panelLocation = new int[2]; + contentView.getLocationOnScreen(panelLocation); + + int screenWidth = mTestableContext.getResources().getDisplayMetrics().widthPixels; + + // Verify initial corner is bottom-right. + assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()) + .isEqualTo(CORNER_BOTTOM_RIGHT); + + dispatchDragSequence(contentView, + /* startX =*/ panelLocation[0] + 10, /* startY =*/ panelLocation[1] + 10, + /* endX =*/ (float) screenWidth / 4, /* endY =*/ panelLocation[1] + 10); + + // Verify snapping to the left. + assertThat(params.gravity).isEqualTo(Gravity.START | Gravity.TOP); + assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()) + .isEqualTo(CORNER_BOTTOM_LEFT); + } + + // Helper method to handle drag event sequences + private void dispatchDragSequence(View view, float startX, float startY, float endX, + float endY) { + // Down event + MotionEvent downEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, startX, startY, + 0); + view.dispatchTouchEvent(downEvent); + + // Move event + MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, endX, endY, 0); + view.dispatchTouchEvent(moveEvent); + + // Up event + MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, endX, endY, 0); + view.dispatchTouchEvent(upEvent); + + // Clean up + downEvent.recycle(); + moveEvent.recycle(); + upEvent.recycle(); + } + private void verifyButtonHasSelectedStyle(@NonNull LinearLayout button) { GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground(); assertThat(gradientDrawable.getColor().getDefaultColor()) .isEqualTo(mTestableContext.getColor(R.color.materialColorPrimary)); } + + private void verifyPanelPosition(int[] expectedPosition) { + WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting(); + assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()).isEqualTo( + expectedPosition[0]); + assertThat(params.gravity).isEqualTo(expectedPosition[1]); + assertThat(params.x).isEqualTo(expectedPosition[2]); + assertThat(params.y).isEqualTo(expectedPosition[3]); + } } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index e0023e59af50..30aa8cebdff6 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -1768,7 +1768,6 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test - @Ignore // b/396073342 public void testCertificateDisclosure() throws Exception { final int userId = CALLER_USER_HANDLE; final UserHandle user = UserHandle.of(userId); @@ -4613,7 +4612,6 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test - @Ignore // b/396073342 public void testGetLastBugReportRequestTime() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); @@ -4661,7 +4659,6 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test - @Ignore // b/396073342 public void testGetLastNetworkLogRetrievalTime() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); @@ -6444,7 +6441,6 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test - @Ignore // b/396073342 public void testGetOwnerInstalledCaCertsForDeviceOwner() throws Exception { mServiceContext.packageName = mRealTestContext.getPackageName(); mServiceContext.applicationInfo = new ApplicationInfo(); @@ -6456,7 +6452,6 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test - @Ignore // b/396073342 public void testGetOwnerInstalledCaCertsForProfileOwner() throws Exception { mServiceContext.packageName = mRealTestContext.getPackageName(); mServiceContext.applicationInfo = new ApplicationInfo(); @@ -6469,7 +6464,6 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test - @Ignore // b/396073342 public void testGetOwnerInstalledCaCertsForDelegate() throws Exception { mServiceContext.packageName = mRealTestContext.getPackageName(); mServiceContext.applicationInfo = new ApplicationInfo(); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java index e5fac7ac5e0c..00b0c558b4e3 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java @@ -263,6 +263,11 @@ public class DpmMockContext extends MockContext { } @Override + public Context getApplicationContext() { + return this; + } + + @Override public PackageManager getPackageManager() { return mMockSystemServices.packageManager; } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java index cf2c15c5daca..b2d48a77386f 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java @@ -16,6 +16,7 @@ package com.android.server.hdmi; +import static android.content.pm.PackageManager.FEATURE_HDMI_CEC; import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM; import static com.android.server.hdmi.HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP; @@ -25,6 +26,7 @@ import static com.android.server.hdmi.HdmiCecFeatureAction.DELAY_GIVE_AUDIO_STAT import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -122,6 +124,9 @@ public abstract class BaseAbsoluteVolumeBehaviorTest { @Before public void setUp() throws RemoteException { + assumeTrue("Test requires FEATURE_HDMI_CEC", + InstrumentationRegistry.getTargetContext().getPackageManager() + .hasSystemFeature(FEATURE_HDMI_CEC)); MockitoAnnotations.initMocks(this); mContextSpy = spy(new ContextWrapper( diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java index 87c9db2fe565..acbce36c3d7f 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java @@ -354,14 +354,20 @@ public abstract class BaseLockSettingsServiceTests { @After public void tearDown_baseServices() throws Exception { - mStorage.closeDatabase(); + if (mStorage != null) { + mStorage.closeDatabase(); + } File db = InstrumentationRegistry.getContext().getDatabasePath("locksettings.db"); assertTrue(!db.exists() || db.delete()); - File storageDir = mStorage.mStorageDir; - assertTrue(FileUtils.deleteContents(storageDir)); + if (mStorage != null) { + File storageDir = mStorage.mStorageDir; + assertTrue(FileUtils.deleteContents(storageDir)); + } - mPasswordSlotManager.cleanup(); + if (mPasswordSlotManager != null) { + mPasswordSlotManager.cleanup(); + } } protected void flushHandlerTasks() { diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java index 02b86db6ab6f..387b89a41eba 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java @@ -124,7 +124,9 @@ public class LockSettingsStorageTests { @After public void tearDown() throws Exception { - mStorage.closeDatabase(); + if (mStorage != null) { + mStorage.closeDatabase(); + } } @Test diff --git a/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTests.java index 2faf6a2b29d1..2c2b9374fdf9 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTests.java @@ -49,7 +49,9 @@ public class PasswordSlotManagerTests { @After public void tearDown() throws Exception { - mManager.cleanup(); + if (mManager != null) { + mManager.cleanup(); + } } @Test diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncTaskTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncTaskTest.java index 1514de04fb08..5add74e5b69e 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncTaskTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncTaskTest.java @@ -156,9 +156,12 @@ public class KeySyncTaskTest { @After public void tearDown() { - mRecoverableKeyStoreDb.close(); - mDatabaseFile.delete(); - + if (mRecoverableKeyStoreDb != null) { + mRecoverableKeyStoreDb.close(); + } + if (mDatabaseFile != null) { + mDatabaseFile.delete(); + } File file = new File(InstrumentationRegistry.getTargetContext().getFilesDir(), SNAPSHOT_TOP_LEVEL_DIRECTORY); FileUtils.deleteContentsAndDir(file); diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/PlatformKeyManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/PlatformKeyManagerTest.java index c09e09c8404f..46eaba7dace6 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/PlatformKeyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/PlatformKeyManagerTest.java @@ -117,8 +117,12 @@ public class PlatformKeyManagerTest { @After public void tearDown() { - mRecoverableKeyStoreDb.close(); - mDatabaseFile.delete(); + if (mRecoverableKeyStoreDb != null) { + mRecoverableKeyStoreDb.close(); + } + if (mDatabaseFile != null) { + mDatabaseFile.delete(); + } } @Test diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java index 64130266b2c4..e6a6e36e75d6 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java @@ -89,8 +89,12 @@ public class RecoverableKeyGeneratorTest { keyStore.load(/*param=*/ null); keyStore.deleteEntry(WRAPPING_KEY_ALIAS); - mRecoverableKeyStoreDb.close(); - mDatabaseFile.delete(); + if (mRecoverableKeyStoreDb != null) { + mRecoverableKeyStoreDb.close(); + } + if (mDatabaseFile != null) { + mDatabaseFile.delete(); + } } @Test diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java index 7641fb957cc8..878c838e734b 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java @@ -230,9 +230,15 @@ public class RecoverableKeyStoreManagerTest { @After public void tearDown() { - mRemoteLockscreenValidationSessionStorage.finishSession(mUserId); - mRecoverableKeyStoreDb.close(); - mDatabaseFile.delete(); + if (mRemoteLockscreenValidationSessionStorage != null) { + mRemoteLockscreenValidationSessionStorage.finishSession(mUserId); + } + if (mRecoverableKeyStoreDb != null) { + mRecoverableKeyStoreDb.close(); + } + if (mDatabaseFile != null) { + mDatabaseFile.delete(); + } } @Test diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java index bbd9223718ae..fb98fab52ca0 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java @@ -18,6 +18,8 @@ package com.android.server.locksettings.recoverablekeystore.storage; import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; @@ -36,8 +38,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import static java.nio.charset.StandardCharsets.UTF_8; - @SmallTest @RunWith(AndroidJUnit4.class) public class RecoverableKeyStoreDbHelperTest { @@ -110,7 +110,9 @@ public class RecoverableKeyStoreDbHelperTest { @After public void tearDown() throws Exception { - mDatabase.close(); + if (mDatabase != null) { + mDatabase.close(); + } } private void createV2Tables() throws Exception { diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java index 8bc14fc54ae1..a77d8bcd3875 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java @@ -72,8 +72,12 @@ public class RecoverableKeyStoreDbTest { @After public void tearDown() { - mRecoverableKeyStoreDb.close(); - mDatabaseFile.delete(); + if (mRecoverableKeyStoreDb != null) { + mRecoverableKeyStoreDb.close(); + } + if (mDatabaseFile != null) { + mDatabaseFile.delete(); + } } @Test diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayConstraintsTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayConstraintsTests.java index b2e296a36b93..2912a0762761 100644 --- a/services/tests/servicestests/src/com/android/server/om/OverlayConstraintsTests.java +++ b/services/tests/servicestests/src/com/android/server/om/OverlayConstraintsTests.java @@ -19,6 +19,7 @@ package com.android.server.om; import static android.content.Context.DEVICE_ID_DEFAULT; import static android.content.om.OverlayConstraint.TYPE_DEVICE_ID; import static android.content.om.OverlayConstraint.TYPE_DISPLAY_ID; +import static android.util.TypedValue.TYPE_STRING; import static android.view.Display.DEFAULT_DISPLAY; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; @@ -28,6 +29,9 @@ import static junit.framework.Assert.assertNotNull; import static org.testng.Assert.assertThrows; +import android.app.Activity; +import android.companion.virtual.VirtualDeviceManager; +import android.content.Context; import android.content.om.FabricatedOverlay; import android.content.om.OverlayConstraint; import android.content.om.OverlayIdentifier; @@ -35,12 +39,12 @@ import android.content.om.OverlayInfo; import android.content.om.OverlayManager; import android.content.om.OverlayManagerTransaction; import android.content.res.Flags; +import android.content.res.Resources; import android.os.UserHandle; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; -import android.util.TypedValue; +import android.view.Display; +import android.virtualdevice.cts.common.VirtualDeviceRule; import junitparams.JUnitParamsRunner; import junitparams.Parameters; @@ -53,20 +57,28 @@ import org.junit.runner.RunWith; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeoutException; @RunWith(JUnitParamsRunner.class) public class OverlayConstraintsTests { + private static final String RESOURCE_NAME = "string/module_2_name"; + private static final String RESOURCE_DEFAULT_VALUE = "module_2_name"; + private static final String RESOURCE_OVERLAID_VALUE = "hello"; + private static final long TIMEOUT_MILLIS = 2000L; @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + public final VirtualDeviceRule mVirtualDeviceRule = VirtualDeviceRule.createDefault(); private OverlayManager mOverlayManager; private UserHandle mUserHandle; private OverlayIdentifier mOverlayIdentifier = null; + private final String mPackageName = getApplicationContext().getPackageName(); @Before public void setUp() throws Exception { - mOverlayManager = getApplicationContext().getSystemService(OverlayManager.class); + final Context context = getApplicationContext(); + mOverlayManager = context.getSystemService(OverlayManager.class); mUserHandle = UserHandle.of(UserHandle.myUserId()); } @@ -79,6 +91,7 @@ public class OverlayConstraintsTests { .build(); mOverlayManager.commit(transaction); mOverlayIdentifier = null; + waitForResourceValue(RESOURCE_DEFAULT_VALUE, getApplicationContext()); } } @@ -161,13 +174,161 @@ public class OverlayConstraintsTests { List.of(new OverlayConstraint(TYPE_DISPLAY_ID, DEFAULT_DISPLAY)))); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS) + public void enableOverlayWithoutConstraints_appliesOverlayWithoutConstraints() + throws Exception { + enableOverlay(Collections.emptyList()); + + // Assert than the overlay is applied for both default device context and virtual + // device context. + final Context context = getApplicationContext(); + waitForResourceValue(RESOURCE_OVERLAID_VALUE, context); + VirtualDeviceManager.VirtualDevice virtualDevice = + mVirtualDeviceRule.createManagedVirtualDevice(); + final Context deviceContext = context.createDeviceContext(virtualDevice.getDeviceId()); + waitForResourceValue(RESOURCE_OVERLAID_VALUE, deviceContext); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS) + public void enableOverlayWithConstraints_withTypeDeviceId_appliesOverlayWithConstraints() + throws Exception { + final int deviceId1 = mVirtualDeviceRule.createManagedVirtualDevice().getDeviceId(); + final int deviceId2 = mVirtualDeviceRule.createManagedVirtualDevice().getDeviceId(); + enableOverlay(List.of(new OverlayConstraint(TYPE_DEVICE_ID, deviceId1), + new OverlayConstraint(TYPE_DEVICE_ID, deviceId2))); + + // Assert than the overlay is not applied for contexts not associated with the above + // devices. + final Context context = getApplicationContext(); + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, context); + final int deviceId3 = mVirtualDeviceRule.createManagedVirtualDevice().getDeviceId(); + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, context.createDeviceContext(deviceId3)); + + // Assert than the overlay is applied for contexts associated with the above devices. + waitForResourceValue(RESOURCE_OVERLAID_VALUE, context.createDeviceContext(deviceId1)); + waitForResourceValue(RESOURCE_OVERLAID_VALUE, context.createDeviceContext(deviceId2)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS) + public void enableOverlayWithConstraints_withTypeDisplayId_appliesOverlayWithConstraints() + throws Exception { + final Display display1 = + mVirtualDeviceRule.createManagedUnownedVirtualDisplay().getDisplay(); + final Display display2 = + mVirtualDeviceRule.createManagedUnownedVirtualDisplay().getDisplay(); + enableOverlay(List.of(new OverlayConstraint(TYPE_DISPLAY_ID, display1.getDisplayId()), + new OverlayConstraint(TYPE_DISPLAY_ID, display2.getDisplayId()))); + + // Assert than the overlay is not applied for contexts not associated with the above + // displays. + final Context context = getApplicationContext(); + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, context); + final Display display3 = + mVirtualDeviceRule.createManagedUnownedVirtualDisplay().getDisplay(); + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, context.createDisplayContext(display3)); + + // Assert than the overlay is applied for contexts associated with the above displays. + waitForResourceValue(RESOURCE_OVERLAID_VALUE, context.createDisplayContext(display1)); + waitForResourceValue(RESOURCE_OVERLAID_VALUE, context.createDisplayContext(display2)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS) + public void enableOverlayWithConstraints_withTypesDisplayIdAndDeviceId_appliesOverlayWithConstraints() + throws Exception { + final Display display1 = + mVirtualDeviceRule.createManagedUnownedVirtualDisplay().getDisplay(); + final int deviceId1 = mVirtualDeviceRule.createManagedVirtualDevice().getDeviceId(); + enableOverlay(List.of(new OverlayConstraint(TYPE_DISPLAY_ID, display1.getDisplayId()), + new OverlayConstraint(TYPE_DEVICE_ID, deviceId1))); + + // Assert than the overlay is not applied for contexts not associated with the above + // display or device. + final Context context = getApplicationContext(); + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, context); + final Display display2 = + mVirtualDeviceRule.createManagedUnownedVirtualDisplay().getDisplay(); + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, context.createDisplayContext(display2)); + final int deviceId2 = mVirtualDeviceRule.createManagedVirtualDevice().getDeviceId(); + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, context.createDeviceContext(deviceId2)); + + // Assert than the overlay is applied for contexts associated with the above display or + // device. + waitForResourceValue(RESOURCE_OVERLAID_VALUE, context.createDisplayContext(display1)); + waitForResourceValue(RESOURCE_OVERLAID_VALUE, context.createDeviceContext(deviceId1)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS) + public void enableOverlayWithConstraints_withTypeDisplayId_appliesForActivityOnDisplay() + throws Exception { + final Display display = + mVirtualDeviceRule.createManagedUnownedVirtualDisplay( + VirtualDeviceRule.createTrustedVirtualDisplayConfigBuilder()) + .getDisplay(); + final Activity activityOnDefaultDisplay = mVirtualDeviceRule.startActivityOnDisplaySync( + DEFAULT_DISPLAY, Activity.class); + final Activity activityOnVirtualDisplay = mVirtualDeviceRule.startActivityOnDisplaySync( + display.getDisplayId(), Activity.class); + + enableOverlay(List.of(new OverlayConstraint(TYPE_DISPLAY_ID, display.getDisplayId()))); + + // Assert than the overlay is not applied for any existing activity on the default display. + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, activityOnDefaultDisplay); + // Assert than the overlay is applied for any existing activity on the virtual display. + waitForResourceValue(RESOURCE_OVERLAID_VALUE, activityOnVirtualDisplay); + + // Assert than the overlay is not applied for any new activity on the default display. + final Activity newActivityOnDefaultDisplay = mVirtualDeviceRule.startActivityOnDisplaySync( + DEFAULT_DISPLAY, Activity.class); + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, newActivityOnDefaultDisplay); + // Assert than the overlay is applied for any new activity on the virtual display. + final Activity newActivityOnVirtualDisplay = mVirtualDeviceRule.startActivityOnDisplaySync( + display.getDisplayId(), Activity.class); + waitForResourceValue(RESOURCE_OVERLAID_VALUE, newActivityOnVirtualDisplay); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS) + public void enableOverlayWithConstraints_withTypeDeviceId_appliesForActivityOnDevice() + throws Exception { + final VirtualDeviceManager.VirtualDevice device = + mVirtualDeviceRule.createManagedVirtualDevice(); + final Display display = + mVirtualDeviceRule.createManagedVirtualDisplay(device, + VirtualDeviceRule.createTrustedVirtualDisplayConfigBuilder()) + .getDisplay(); + final Activity activityOnDefaultDevice = mVirtualDeviceRule.startActivityOnDisplaySync( + DEFAULT_DISPLAY, Activity.class); + final Activity activityOnVirtualDevice = mVirtualDeviceRule.startActivityOnDisplaySync( + display.getDisplayId(), Activity.class); + + enableOverlay(List.of(new OverlayConstraint(TYPE_DEVICE_ID, device.getDeviceId()))); + + // Assert than the overlay is not applied for any existing activity on the default device. + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, activityOnDefaultDevice); + // Assert than the overlay is applied for any existing activity on the virtual device. + waitForResourceValue(RESOURCE_OVERLAID_VALUE, activityOnVirtualDevice); + + // Assert than the overlay is not applied for any new activity on the default device. + final Activity newActivityOnDefaultDevice = mVirtualDeviceRule.startActivityOnDisplaySync( + DEFAULT_DISPLAY, Activity.class); + ensureResourceValueStaysAt(RESOURCE_DEFAULT_VALUE, newActivityOnDefaultDevice); + // Assert than the overlay is applied for any new activity on the virtual device. + final Activity newActivityOnVirtualDevice = mVirtualDeviceRule.startActivityOnDisplaySync( + display.getDisplayId(), Activity.class); + waitForResourceValue(RESOURCE_OVERLAID_VALUE, newActivityOnVirtualDevice); + } + private FabricatedOverlay createFabricatedOverlay() { - String packageName = getApplicationContext().getPackageName(); FabricatedOverlay fabricatedOverlay = new FabricatedOverlay.Builder( - packageName, "testOverlay" /* name */, packageName) + mPackageName, "testOverlay" /* name */, mPackageName) .build(); - fabricatedOverlay.setResourceValue("string/module_2_name" /* resourceName */, - TypedValue.TYPE_STRING, "hello" /* value */, null /* configuration */); + fabricatedOverlay.setResourceValue(RESOURCE_NAME, TYPE_STRING, RESOURCE_OVERLAID_VALUE, + null /* configuration */); return fabricatedOverlay; } @@ -183,6 +344,37 @@ public class OverlayConstraintsTests { mOverlayIdentifier = fabricatedOverlay.getIdentifier(); } + private static void waitForResourceValue(final String expectedValue, Context context) + throws TimeoutException { + final long endTime = System.currentTimeMillis() + TIMEOUT_MILLIS; + final Resources resources = context.getResources(); + final int resourceId = getResourceId(context); + String resourceValue = null; + while (System.currentTimeMillis() < endTime) { + resourceValue = resources.getString(resourceId); + if (Objects.equals(resourceValue, expectedValue)) { + return; + } + } + throw new TimeoutException("Timed out waiting for '" + RESOURCE_NAME + "' value to equal '" + + expectedValue + "': current value is '" + resourceValue + "'"); + } + + private static void ensureResourceValueStaysAt(final String expectedValue, Context context) { + final long endTime = System.currentTimeMillis() + TIMEOUT_MILLIS; + final Resources resources = context.getResources(); + final int resourceId = getResourceId(context); + String resourceValue; + while (System.currentTimeMillis() < endTime) { + resourceValue = resources.getString(resourceId); + assertEquals(expectedValue, resourceValue); + } + } + + private static int getResourceId(Context context) { + return context.getResources().getIdentifier(RESOURCE_NAME, "", context.getPackageName()); + } + private static List<OverlayConstraint>[] getAllConstraintLists() { return new List[]{ Collections.emptyList(), diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 3ef360a752f6..da14e451d656 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -95,8 +95,8 @@ import android.test.mock.MockContext; import android.util.ArrayMap; import android.util.Log; import android.util.Pair; +import android.util.SparseArray; -import com.android.internal.infra.AndroidFuture; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.pm.LauncherAppsService.LauncherAppsImpl; @@ -110,6 +110,7 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -149,6 +150,9 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected static final String MAIN_ACTIVITY_CLASS = "MainActivity"; protected static final String PIN_CONFIRM_ACTIVITY_CLASS = "PinConfirmActivity"; + private byte[] mBaseState; + protected final SparseArray<byte[]> mUserStates = new SparseArray<>(); + // public for mockito public class BaseContext extends MockContext { @Override @@ -287,6 +291,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { final ServiceContext mContext; IUidObserver mUidObserver; + public ShortcutServiceTestable(ServiceContext context, Looper looper) { super(context, looper, /* onyForPackageManagerApis */ false); mContext = context; @@ -567,6 +572,58 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { // During tests, WTF is fatal. fail(message + " exception: " + th + "\n" + Log.getStackTraceString(th)); } + + @Override + void injectSaveBaseState() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + saveBaseStateAsXml(baos); + } catch (Exception e) { + throw new RuntimeException(e); + } + mBaseState = baos.toByteArray(); + } + + @Override + protected void injectLoadBaseState() { + if (mBaseState == null) { + return; + } + ByteArrayInputStream bais = new ByteArrayInputStream(mBaseState); + try { + loadBaseStateAsXml(bais); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + protected void injectSaveUser(@UserIdInt int userId) { + synchronized (mServiceLock) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + saveUserInternalLocked(userId, baos, /* forBackup= */ false); + cleanupDanglingBitmapDirectoriesLocked(userId); + } catch (Exception e) { + throw new RuntimeException(e); + } + mUserStates.put(userId, baos.toByteArray()); + } + } + + @Override + protected ShortcutUser injectLoadUserLocked(@UserIdInt int userId) { + final byte[] userState = mUserStates.get(userId); + if (userState == null) { + return null; + } + ByteArrayInputStream bais = new ByteArrayInputStream(userState); + try { + return loadUserInternal(userId, bais, /* forBackup= */ false); + } catch (Exception e) { + throw new RuntimeException(e); + } + } } /** ShortcutManager with injection override methods. */ diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index f536cae53e3a..ad1537e270e8 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -201,7 +201,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL + 50; assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL); - mService.saveBaseState(); + mService.injectSaveBaseState(); dumpBaseStateFile(); @@ -261,7 +261,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // === Test for app side APIs === /** Test for {@link android.content.pm.ShortcutManager#getMaxShortcutCountForActivity()} */ - public void testGetMaxDynamicShortcutCount() { + public void disabled_testGetMaxDynamicShortcutCount() { assertEquals(MAX_SHORTCUTS, mManager.getMaxShortcutCountForActivity()); } @@ -793,7 +793,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(2, mManager.getRemainingCallCount()); } - public void testDeleteAllDynamicShortcuts() { + public void disabled_testDeleteAllDynamicShortcuts() { final ShortcutInfo si1 = makeShortcut("shortcut1"); final ShortcutInfo si2 = makeShortcut("shortcut2"); final ShortcutInfo si3 = makeShortcut("shortcut3"); @@ -1036,7 +1036,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { */ } - public void testCleanupDanglingBitmaps() throws Exception { + public void disabled_testCleanupDanglingBitmaps() throws Exception { assertBitmapDirectories(USER_10, EMPTY_STRINGS); assertBitmapDirectories(USER_11, EMPTY_STRINGS); @@ -1702,7 +1702,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { "s2"); } - public void testCachedShortcuts_accessShortcutsPermission() { + public void disabled_testCachedShortcuts_accessShortcutsPermission() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"), makeLongLivedShortcut("s2"), makeLongLivedShortcut("s3"), @@ -1744,7 +1744,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertShortcutIds(mManager.getShortcuts(ShortcutManager.FLAG_MATCH_CACHED), "s3"); } - public void testCachedShortcuts_canPassShortcutLimit() { + public void disabled_testCachedShortcuts_canPassShortcutLimit() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=4"); @@ -2362,7 +2362,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { * This is similar to the above test, except it used "disable" instead of "remove". It also * does "enable". */ - public void testDisableAndEnableShortcuts() { + public void disabled_testDisableAndEnableShortcuts() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000); final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000); @@ -2487,7 +2487,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testDisableShortcuts_thenRepublish() { + public void disabled_testDisableShortcuts_thenRepublish() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3")))); @@ -4230,7 +4230,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // TODO Check all other fields } - public void testCleanupPackage() { + public void disabled_testCleanupPackage() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( makeShortcut("s0_1")))); @@ -4507,7 +4507,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { mService.saveDirtyInfo(); } - public void testCleanupPackage_republishManifests() { + public void disabled_testCleanupPackage_republishManifests() { addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), R.xml.shortcut_2); @@ -4847,7 +4847,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(expected, spi.canRestoreTo(mService, pi, true)); } - public void testCanRestoreTo() { + public void disabled_testCanRestoreTo() { addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 10, "sig1"); addPackage(CALLING_PACKAGE_2, CALLING_UID_2, 10, "sig1", "sig2"); addPackage(CALLING_PACKAGE_3, CALLING_UID_3, 10, "sig1"); @@ -5785,7 +5785,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { checkBackupAndRestore_success(/*firstRestore=*/ true); } - public void testBackupAndRestore_restoreToSuperSetSignatures() { + public void disabled_testBackupAndRestore_restoreToSuperSetSignatures() { prepareForBackupTest(); // Change package signatures. diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java index 3b32701b5169..f5690b77d2fe 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java @@ -54,9 +54,7 @@ import androidx.test.filters.SmallTest; import com.android.frameworks.servicestests.R; import java.io.File; -import java.io.FileWriter; import java.io.IOException; -import java.io.Writer; import java.util.Locale; /** @@ -2358,12 +2356,10 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { * can still be read. */ public void testLoadLegacySavedFile() throws Exception { - final File path = mService.getUserFile(USER_10).getBaseFile(); - path.getParentFile().mkdirs(); - try (Writer w = new FileWriter(path)) { - w.write(readTestAsset("shortcut/shortcut_legacy_file.xml")); - }; + final String legacyFile = readTestAsset("shortcut/shortcut_legacy_file.xml"); + mUserStates.put(USER_10, legacyFile.getBytes()); initService(); + mService.handleUnlockUser(USER_10); runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 5dea44d6ebf4..67e85ff2445d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -4495,6 +4495,27 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void testBubblePreference_sameVersionWithSAWPermission() throws Exception { + when(mAppOpsManager.noteOpNoThrow(eq(OP_SYSTEM_ALERT_WINDOW), anyInt(), + anyString(), eq(null), anyString())).thenReturn(MODE_ALLOWED); + + final String xml = "<ranking version=\"4\">\n" + + "<package name=\"" + PKG_O + "\" uid=\"" + UID_O + "\">\n" + + "<channel id=\"someId\" name=\"hi\"" + + " importance=\"3\"/>" + + "</package>" + + "</ranking>"; + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())), + null); + parser.nextTag(); + mHelper.readXml(parser, false, UserHandle.USER_ALL); + + assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O)); + assertEquals(0, mHelper.getAppLockedFields(PKG_O, UID_O)); + } + + @Test public void testBubblePreference_upgradeWithSAWThenUserOverride() throws Exception { when(mAppOpsManager.noteOpNoThrow(eq(OP_SYSTEM_ALERT_WINDOW), anyInt(), anyString(), eq(null), anyString())).thenReturn(MODE_ALLOWED); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java index d5548a4f375e..f091a65cfe46 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java @@ -497,19 +497,19 @@ public class DeviceAdapterTest { private VibratorController createEmptyVibratorController(int vibratorId) { return new FakeVibratorControllerProvider(mTestLooper.getLooper()) - .newVibratorController(vibratorId, (id, vibrationId) -> {}); + .newVibratorController(vibratorId, (id, vibrationId, stepId) -> {}); } private VibratorController createBasicVibratorController(int vibratorId) { FakeVibratorControllerProvider provider = createVibratorProviderWithEffects( IVibrator.CAP_COMPOSE_EFFECTS); - return provider.newVibratorController(vibratorId, (id, vibrationId) -> {}); + return provider.newVibratorController(vibratorId, (id, vibrationId, stepId) -> {}); } private VibratorController createPwleWithoutFrequenciesVibratorController(int vibratorId) { FakeVibratorControllerProvider provider = createVibratorProviderWithEffects( IVibrator.CAP_COMPOSE_EFFECTS, IVibrator.CAP_COMPOSE_PWLE_EFFECTS); - return provider.newVibratorController(vibratorId, (id, vibrationId) -> {}); + return provider.newVibratorController(vibratorId, (id, vibrationId, stepId) -> {}); } private VibratorController createPwleVibratorController(int vibratorId) { @@ -519,7 +519,7 @@ public class DeviceAdapterTest { provider.setMinFrequency(TEST_MIN_FREQUENCY); provider.setFrequencyResolution(TEST_FREQUENCY_RESOLUTION); provider.setMaxAmplitudes(TEST_AMPLITUDE_MAP); - return provider.newVibratorController(vibratorId, (id, vibrationId) -> {}); + return provider.newVibratorController(vibratorId, (id, vibrationId, stepId) -> {}); } private VibratorController createPwleV2VibratorController(int vibratorId) { @@ -538,7 +538,7 @@ public class DeviceAdapterTest { provider.setMinEnvelopeEffectControlPointDurationMillis( TEST_MIN_ENVELOPE_EFFECT_CONTROL_POINT_DURATION_MILLIS); - return provider.newVibratorController(vibratorId, (id, vibrationId) -> {}); + return provider.newVibratorController(vibratorId, (id, vibrationId, stepId) -> {}); } private FakeVibratorControllerProvider createVibratorProviderWithEffects(int... capabilities) { diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java index 5f2af0a085c3..42279e40fa33 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java @@ -17,6 +17,9 @@ package com.android.server.vibrator; import static android.os.VibrationAttributes.USAGE_RINGTONE; +import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK; +import static android.os.VibrationEffect.Composition.PRIMITIVE_SPIN; +import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK; import static android.os.VibrationEffect.VibrationParameter.targetAmplitude; import static android.os.VibrationEffect.VibrationParameter.targetFrequency; @@ -66,7 +69,6 @@ import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; @@ -153,9 +155,10 @@ public class VibrationThreadTest { when(mPackageManagerInternalMock.getSystemUiServiceComponent()) .thenReturn(new ComponentName("", "")); doAnswer(answer -> { - mVibrationConductor.notifyVibratorComplete(answer.getArgument(0)); + mVibrationConductor.notifyVibratorComplete( + answer.getArgument(0), answer.getArgument(2)); return null; - }).when(mControllerCallbacks).onComplete(anyInt(), anyLong()); + }).when(mControllerCallbacks).onComplete(anyInt(), anyLong(), anyLong()); LocalServices.removeServiceForTest(PackageManagerInternal.class); LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock); @@ -190,7 +193,7 @@ public class VibrationThreadTest { HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id)); + verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED); } @@ -203,7 +206,7 @@ public class VibrationThreadTest { HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id)); + verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED); } @@ -217,7 +220,7 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); @@ -234,7 +237,7 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); @@ -254,7 +257,7 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(15L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); @@ -265,7 +268,7 @@ public class VibrationThreadTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) public void vibrate_singleWaveformWithAdaptiveHapticsScaling_scalesAmplitudesProperly() { // No user settings scale. setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, @@ -291,7 +294,7 @@ public class VibrationThreadTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) public void vibrate_withVibrationParamsRequestStalling_timeoutRequestAndApplyNoScaling() { // No user settings scale. setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, @@ -412,6 +415,7 @@ public class VibrationThreadTest { .isEqualTo(expectedOneShots(200L, 50L)); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @LargeTest @Test public void vibrate_singleVibratorRepeatingPatternWithZeroDurationSteps_repeatsEffectCorrectly() @@ -443,6 +447,30 @@ public class VibrationThreadTest { .isEqualTo(expectedOneShots(200L, 150L, 300L, 150L, 300L)); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) + @Test + public void vibrate_singleVibratorPatternWithCallbackDelay_oldCallbacksIgnored() { + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); + fakeVibrator.setCompletionCallbackDelay(100); // 100ms delay to notify service. + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + + VibrationEffect effect = VibrationEffect.createWaveform( + /* timings= */ new long[]{0, 200, 50, 400}, /* repeat= */ -1); + HalVibration vibration = startThreadAndDispatcher(effect); + waitForCompletion(800 + TEST_TIMEOUT_MILLIS); // 200 + 50 + 400 + 100ms delay + + verifyCallbacksTriggered(vibration, Status.FINISHED); + assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse(); + + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), eq(1L)); + // Step id = 2 skipped by the 50ms OFF step after the 200ms ON step. + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), eq(3L)); + + // First callback ignored, did not cause the vibrator to turn back on during the 400ms step. + assertThat(fakeVibrator.getEffectSegments(vibration.id)) + .isEqualTo(expectedOneShots(200L, 400L)); + } + @Test public void vibrate_singleVibratorRepeatingPwle_generatesLargestPwles() throws Exception { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); @@ -479,12 +507,12 @@ public class VibrationThreadTest { throws Exception { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - fakeVibrator.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK); + fakeVibrator.setSupportedPrimitives(PRIMITIVE_CLICK); fakeVibrator.setCompositionSizeMax(10); VibrationEffect effect = VibrationEffect.startComposition() // Very long delay so thread will be cancelled after first PWLE is triggered. - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100) + .addPrimitive(PRIMITIVE_CLICK, 1f, 100) .compose(); VibrationEffect repeatingEffect = VibrationEffect.startComposition() .repeatEffectIndefinitely(effect) @@ -561,13 +589,12 @@ public class VibrationThreadTest { public void vibrate_singleVibratorPredefinedCancel_cancelsVibrationImmediately() throws Exception { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - mVibratorProviders.get(VIBRATOR_ID).setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK); + mVibratorProviders.get(VIBRATOR_ID).setSupportedPrimitives(PRIMITIVE_CLICK); VibrationEffect effect = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100) + .addPrimitive(PRIMITIVE_CLICK, 1f, 100) + .addPrimitive(PRIMITIVE_CLICK, 1f, 100) + .addPrimitive(PRIMITIVE_CLICK, 1f, 100) .compose(); HalVibration vibration = startThreadAndDispatcher(effect); @@ -591,7 +618,7 @@ public class VibrationThreadTest { } @Test - @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) public void vibrate_singleVibratorVendorEffectCancel_cancelsVibrationImmediately() throws Exception { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); @@ -657,7 +684,7 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); @@ -678,7 +705,7 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); @@ -695,13 +722,14 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L)); verify(mManagerHooks, never()).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks, never()) + .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED); assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty()); } @Test - @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) public void vibrate_singleVibratorVendorEffect_runsVibration() { mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); @@ -712,7 +740,7 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(PerformVendorEffectVibratorStep.VENDOR_EFFECT_MAX_DURATION_MS)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse(); @@ -725,24 +753,23 @@ public class VibrationThreadTest { public void vibrate_singleVibratorComposed_runsVibration() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - fakeVibrator.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK, - VibrationEffect.Composition.PRIMITIVE_TICK); + fakeVibrator.setSupportedPrimitives(PRIMITIVE_CLICK, PRIMITIVE_TICK); VibrationEffect effect = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) + .addPrimitive(PRIMITIVE_CLICK, 1f) + .addPrimitive(PRIMITIVE_TICK, 0.5f) .compose(); HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(40L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( - expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0), - expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 0)), + expectedPrimitive(PRIMITIVE_CLICK, 1, 0), + expectedPrimitive(PRIMITIVE_TICK, 0.5f, 0)), fakeVibrator.getEffectSegments(vibration.id)); } @@ -750,14 +777,15 @@ public class VibrationThreadTest { @DisableFlags(Flags.FLAG_PRIMITIVE_COMPOSITION_ABSOLUTE_DELAY) public void vibrate_singleVibratorComposedAndNoCapability_triggersHalAndReturnsUnsupported() { VibrationEffect effect = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) + .addPrimitive(PRIMITIVE_CLICK, 1f) .compose(); HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L)); verify(mManagerHooks, never()).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks, never()) + .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED); assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty()); } @@ -766,14 +794,15 @@ public class VibrationThreadTest { @EnableFlags(Flags.FLAG_PRIMITIVE_COMPOSITION_ABSOLUTE_DELAY) public void vibrate_singleVibratorComposedAndNoCapability_ignoresVibration() { VibrationEffect effect = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) + .addPrimitive(PRIMITIVE_CLICK, 1f) .compose(); HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks, never()).noteVibratorOn(eq(UID), anyLong()); verify(mManagerHooks, never()).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks, never()) + .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED); assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty()); } @@ -782,34 +811,30 @@ public class VibrationThreadTest { public void vibrate_singleVibratorLargeComposition_splitsVibratorComposeCalls() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - fakeVibrator.setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK, - VibrationEffect.Composition.PRIMITIVE_TICK, - VibrationEffect.Composition.PRIMITIVE_SPIN); + fakeVibrator.setSupportedPrimitives(PRIMITIVE_CLICK, PRIMITIVE_TICK, PRIMITIVE_SPIN); fakeVibrator.setCompositionSizeMax(2); VibrationEffect effect = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f) + .addPrimitive(PRIMITIVE_CLICK, 1f) + .addPrimitive(PRIMITIVE_TICK, 0.5f) + .addPrimitive(PRIMITIVE_SPIN, 0.8f) .compose(); HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verifyCallbacksTriggered(vibration, Status.FINISHED); // Vibrator compose called twice. - verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks, times(2)) + .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); assertEquals(3, fakeVibrator.getEffectSegments(vibration.id).size()); } @Test - @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public void vibrate_singleVibratorComposedEffects_runsDifferentVibrations() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); - fakeVibrator.setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK, - VibrationEffect.Composition.PRIMITIVE_TICK); + fakeVibrator.setSupportedPrimitives(PRIMITIVE_CLICK, PRIMITIVE_TICK); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS, IVibrator.CAP_COMPOSE_PWLE_EFFECTS, IVibrator.CAP_AMPLITUDE_CONTROL); fakeVibrator.setMinFrequency(100); @@ -820,8 +845,8 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.startComposition() .addEffect(VibrationEffect.createOneShot(10, 100)) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) + .addPrimitive(PRIMITIVE_CLICK, 1f) + .addPrimitive(PRIMITIVE_TICK, 0.5f) .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) .addEffect(VibrationEffect.startWaveform() .addTransition(Duration.ofMillis(10), @@ -836,13 +861,14 @@ public class VibrationThreadTest { // Use first duration the vibrator is turned on since we cannot estimate the clicks. verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks, times(5)) + .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( expectedOneShot(10), - expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0), - expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 0), + expectedPrimitive(PRIMITIVE_CLICK, 1, 0), + expectedPrimitive(PRIMITIVE_TICK, 0.5f, 0), expectedPrebaked(VibrationEffect.EFFECT_CLICK), expectedRamp(/* startAmplitude= */ 0, /* endAmplitude= */ 0.5f, /* startFrequencyHz= */ 150, /* endFrequencyHz= */ 100, /* duration= */ 10), @@ -857,17 +883,15 @@ public class VibrationThreadTest { public void vibrate_singleVibratorComposedWithFallback_replacedInTheMiddleOfComposition() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); - fakeVibrator.setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK, - VibrationEffect.Composition.PRIMITIVE_TICK); + fakeVibrator.setSupportedPrimitives(PRIMITIVE_CLICK, PRIMITIVE_TICK); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); VibrationEffect fallback = VibrationEffect.createOneShot(10, 100); VibrationEffect effect = VibrationEffect.startComposition() .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) + .addPrimitive(PRIMITIVE_CLICK, 1f) .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_TICK)) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) + .addPrimitive(PRIMITIVE_TICK, 0.5f) .compose(); HalVibration vibration = createVibration(CombinedVibration.createParallel(effect)); vibration.fillFallbacks(unused -> fallback); @@ -877,7 +901,8 @@ public class VibrationThreadTest { // Use first duration the vibrator is turned on since we cannot estimate the clicks. verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong()); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks, times(4)) + .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); @@ -894,7 +919,7 @@ public class VibrationThreadTest { } @Test - @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public void vibrate_singleVibratorPwle_runsComposePwleV2() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); @@ -915,7 +940,7 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( @@ -929,7 +954,7 @@ public class VibrationThreadTest { } @Test - @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public void vibrate_singleVibratorBasicPwle_runsComposePwleV2() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); @@ -951,7 +976,7 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(220L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( @@ -964,7 +989,7 @@ public class VibrationThreadTest { } @Test - @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public void vibrate_singleVibratorPwle_withInitialFrequency_runsComposePwleV2() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); @@ -987,7 +1012,7 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( @@ -1001,7 +1026,7 @@ public class VibrationThreadTest { } @Test - @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public void vibrate_singleVibratorPwle_TooManyControlPoints_splitsAndRunsComposePwleV2() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); @@ -1027,7 +1052,8 @@ public class VibrationThreadTest { verifyCallbacksTriggered(vibration, Status.FINISHED); // Vibrator compose called 3 times with 2 segments instead of 2 times with 3 segments. // Using best split points instead of max-packing PWLEs. - verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks, times(3)) + .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( @@ -1043,7 +1069,7 @@ public class VibrationThreadTest { } @Test - @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public void vibrate_singleVibratorPwle_runsComposePwle() { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS); @@ -1066,7 +1092,7 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( @@ -1109,7 +1135,8 @@ public class VibrationThreadTest { // Vibrator compose called 3 times with 2 segments instead of 2 times with 3 segments. // Using best split points instead of max-packing PWLEs. - verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks, times(3)) + .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); assertEquals(6, fakeVibrator.getEffectSegments(vibration.id).size()); } @@ -1160,8 +1187,8 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); - verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); + verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); @@ -1183,9 +1210,9 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id)); - verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id)); - verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id), anyLong()); + verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id), anyLong()); + verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); @@ -1207,11 +1234,10 @@ public class VibrationThreadTest { mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); mVibratorProviders.get(3).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); mVibratorProviders.get(4).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - mVibratorProviders.get(4).setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK); + mVibratorProviders.get(4).setSupportedPrimitives(PRIMITIVE_CLICK); VibrationEffect composed = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(PRIMITIVE_CLICK) .compose(); CombinedVibration effect = CombinedVibration.startParallel() .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) @@ -1225,10 +1251,10 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id)); - verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id)); - verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id)); - verify(mControllerCallbacks).onComplete(eq(4), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id), anyLong()); + verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id), anyLong()); + verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id), anyLong()); + verify(mControllerCallbacks).onComplete(eq(4), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); @@ -1243,8 +1269,7 @@ public class VibrationThreadTest { assertEquals(Arrays.asList(expectedOneShot(20)), mVibratorProviders.get(3).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(1, 2), mVibratorProviders.get(3).getAmplitudes()); - assertEquals(Arrays.asList( - expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)), + assertEquals(Arrays.asList(expectedPrimitive(PRIMITIVE_CLICK, 1, 0)), mVibratorProviders.get(4).getEffectSegments(vibration.id)); } @@ -1253,12 +1278,11 @@ public class VibrationThreadTest { mockVibrators(1, 2, 3); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - mVibratorProviders.get(2).setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK); + mVibratorProviders.get(2).setSupportedPrimitives(PRIMITIVE_CLICK); mVibratorProviders.get(3).setSupportedEffects(VibrationEffect.EFFECT_CLICK); VibrationEffect composed = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(PRIMITIVE_CLICK) .compose(); CombinedVibration effect = CombinedVibration.startSequential() .addNext(3, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), /* delay= */ 50) @@ -1268,10 +1292,10 @@ public class VibrationThreadTest { HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - InOrder controllerVerifier = inOrder(mControllerCallbacks); - controllerVerifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id)); - controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id)); - controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id)); + InOrder verifier = inOrder(mControllerCallbacks); + verifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id), anyLong()); + verifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id), anyLong()); + verifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id), anyLong()); InOrder batteryVerifier = inOrder(mManagerHooks); batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); @@ -1289,8 +1313,7 @@ public class VibrationThreadTest { assertEquals(Arrays.asList(expectedOneShot(10)), mVibratorProviders.get(1).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(100), mVibratorProviders.get(1).getAmplitudes()); - assertEquals(Arrays.asList( - expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)), + assertEquals(Arrays.asList(expectedPrimitive(PRIMITIVE_CLICK, 1, 0)), mVibratorProviders.get(2).getEffectSegments(vibration.id)); assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)), mVibratorProviders.get(3).getEffectSegments(vibration.id)); @@ -1301,15 +1324,13 @@ public class VibrationThreadTest { int[] vibratorIds = new int[]{1, 2}; mockVibrators(vibratorIds); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - mVibratorProviders.get(1).setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK); + mVibratorProviders.get(1).setSupportedPrimitives(PRIMITIVE_CLICK); mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - mVibratorProviders.get(2).setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK); + mVibratorProviders.get(2).setSupportedPrimitives(PRIMITIVE_CLICK); when(mManagerHooks.prepareSyncedVibration(anyLong(), eq(vibratorIds))).thenReturn(true); VibrationEffect composed = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 100) + .addPrimitive(PRIMITIVE_CLICK, 1, 100) .compose(); CombinedVibration effect = CombinedVibration.createParallel(composed); // We create the HalVibration here to obtain the vibration id and use it to mock the @@ -1331,8 +1352,7 @@ public class VibrationThreadTest { verify(mManagerHooks, never()).cancelSyncedVibration(); verifyCallbacksTriggered(vibration, Status.FINISHED); - VibrationEffectSegment expected = expectedPrimitive( - VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 100); + VibrationEffectSegment expected = expectedPrimitive(PRIMITIVE_CLICK, 1, 100); assertEquals(Arrays.asList(expected), mVibratorProviders.get(1).getEffectSegments(vibration.id)); assertEquals(Arrays.asList(expected), @@ -1345,12 +1365,11 @@ public class VibrationThreadTest { mockVibrators(vibratorIds); mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); mVibratorProviders.get(4).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - mVibratorProviders.get(4).setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK); + mVibratorProviders.get(4).setSupportedPrimitives(PRIMITIVE_CLICK); when(mManagerHooks.prepareSyncedVibration(anyLong(), any())).thenReturn(true); VibrationEffect composed = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(PRIMITIVE_CLICK) .compose(); CombinedVibration effect = CombinedVibration.startParallel() .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) @@ -1463,9 +1482,9 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(80L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id)); - verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id)); - verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id), anyLong()); + verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id), anyLong()); + verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); @@ -1505,7 +1524,7 @@ public class VibrationThreadTest { waitForCompletion(rampDownDuration + TEST_TIMEOUT_MILLIS); long completionTime = SystemClock.elapsedRealtime(); - verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibration.id); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); // Vibration ends after duration, thread completed after ramp down assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration); assertThat(vibrationEndTime - startTime).isLessThan(expectedDuration + rampDownDuration); @@ -1534,7 +1553,7 @@ public class VibrationThreadTest { waitForCompletion(TEST_TIMEOUT_MILLIS); - verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibration.id); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration + callbackDelay); } @@ -1563,7 +1582,8 @@ public class VibrationThreadTest { waitForCompletion(callbackDelay + TEST_TIMEOUT_MILLIS); long completionTime = SystemClock.elapsedRealtime(); - verify(mControllerCallbacks, never()).onComplete(VIBRATOR_ID, vibration.id); + verify(mControllerCallbacks, never()) + .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); // Vibration ends and thread completes after timeout, before the HAL callback assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration + callbackTimeout); assertThat(vibrationEndTime - startTime).isLessThan(expectedDuration + callbackDelay); @@ -1639,15 +1659,14 @@ public class VibrationThreadTest { mockVibrators(1, 2); mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - mVibratorProviders.get(2).setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK); + mVibratorProviders.get(2).setSupportedPrimitives(PRIMITIVE_CLICK); CombinedVibration effect = CombinedVibration.startParallel() .addVibrator(1, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) .addVibrator(2, VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100) + .addPrimitive(PRIMITIVE_CLICK, 1f, 100) + .addPrimitive(PRIMITIVE_CLICK, 1f, 100) + .addPrimitive(PRIMITIVE_CLICK, 1f, 100) .compose()) .combine(); HalVibration vibration = startThreadAndDispatcher(effect); @@ -1672,7 +1691,7 @@ public class VibrationThreadTest { } @Test - @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) public void vibrate_multipleVendorEffectCancel_cancelsVibrationImmediately() throws Exception { mockVibrators(1, 2); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); @@ -1767,7 +1786,7 @@ public class VibrationThreadTest { HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); // Duration extended for 5 + 5 + 5 + 15. @@ -1847,7 +1866,7 @@ public class VibrationThreadTest { HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)), @@ -1856,7 +1875,7 @@ public class VibrationThreadTest { } @Test - @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) public void vibrate_vendorEffectWithRampDown_doesNotAddRampDown() { when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15); mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); @@ -1865,7 +1884,7 @@ public class VibrationThreadTest { HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibration.id)) @@ -1879,26 +1898,24 @@ public class VibrationThreadTest { when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15); mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL, IVibrator.CAP_COMPOSE_EFFECTS); - mVibratorProviders.get(VIBRATOR_ID).setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK); + mVibratorProviders.get(VIBRATOR_ID).setSupportedPrimitives(PRIMITIVE_CLICK); VibrationEffect effect = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(PRIMITIVE_CLICK) .compose(); HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); - assertEquals( - Arrays.asList(expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)), + assertEquals(Arrays.asList(expectedPrimitive(PRIMITIVE_CLICK, 1, 0)), mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty()); } @Test - @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) public void vibrate_pwleWithRampDown_doesNotAddRampDown() { when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15); FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); @@ -1916,7 +1933,7 @@ public class VibrationThreadTest { HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); verifyCallbacksTriggered(vibration, Status.FINISHED); assertEquals(Arrays.asList(expectedRamp(0, 1, 150, 150, 1)), @@ -1928,8 +1945,7 @@ public class VibrationThreadTest { public void vibrate_multipleVibrations_withCancel() throws Exception { mVibratorProviders.get(VIBRATOR_ID).setSupportedEffects( VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_TICK); - mVibratorProviders.get(VIBRATOR_ID).setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK); + mVibratorProviders.get(VIBRATOR_ID).setSupportedPrimitives(PRIMITIVE_CLICK); mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL, IVibrator.CAP_COMPOSE_EFFECTS); @@ -1940,7 +1956,7 @@ public class VibrationThreadTest { .repeatEffectIndefinitely(VibrationEffect.get(VibrationEffect.EFFECT_TICK)) .compose(); VibrationEffect effect3 = VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(PRIMITIVE_CLICK) .compose(); VibrationEffect effect4 = VibrationEffect.createOneShot(8000, 100); VibrationEffect effect5 = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); @@ -1974,14 +1990,15 @@ public class VibrationThreadTest { assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); // Effect1 - verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibration1.id); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration1.id), anyLong()); verifyCallbacksTriggered(vibration1, Status.FINISHED); assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)), fakeVibrator.getEffectSegments(vibration1.id)); // Effect2: repeating, cancelled. - verify(mControllerCallbacks, atLeast(2)).onComplete(VIBRATOR_ID, vibration2.id); + verify(mControllerCallbacks, atLeast(2)) + .onComplete(eq(VIBRATOR_ID), eq(vibration2.id), anyLong()); verifyCallbacksTriggered(vibration2, Status.CANCELLED_BY_USER); // The exact count of segments might vary, so just check that there's more than 2 and @@ -1994,10 +2011,9 @@ public class VibrationThreadTest { } // Effect3 - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration3.id)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration3.id), anyLong()); verifyCallbacksTriggered(vibration3, Status.FINISHED); - assertEquals(Arrays.asList( - expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)), + assertEquals(Arrays.asList(expectedPrimitive(PRIMITIVE_CLICK, 1, 0)), fakeVibrator.getEffectSegments(vibration3.id)); // Effect4: cancelled quickly. @@ -2014,8 +2030,7 @@ public class VibrationThreadTest { mockVibrators(1, 2, 3); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); mVibratorProviders.get(2).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - mVibratorProviders.get(2).setSupportedPrimitives( - VibrationEffect.Composition.PRIMITIVE_CLICK); + mVibratorProviders.get(2).setSupportedPrimitives(PRIMITIVE_CLICK); mVibratorProviders.get(3).setSupportedEffects(VibrationEffect.EFFECT_CLICK); CombinedVibration effect = CombinedVibration.startSequential() @@ -2029,8 +2044,7 @@ public class VibrationThreadTest { /* delay= */ TEST_TIMEOUT_MILLIS) .addNext(2, VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, - /* delay= */ TEST_TIMEOUT_MILLIS) + .addPrimitive(PRIMITIVE_CLICK, 1, /* delay= */ TEST_TIMEOUT_MILLIS) .compose(), /* delay= */ TEST_TIMEOUT_MILLIS) .combine(); @@ -2051,8 +2065,7 @@ public class VibrationThreadTest { assertEquals(Arrays.asList(expectedOneShot(TEST_TIMEOUT_MILLIS)), mVibratorProviders.get(1).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(255), mVibratorProviders.get(1).getAmplitudes()); - assertEquals(Arrays.asList(expectedPrimitive( - VibrationEffect.Composition.PRIMITIVE_CLICK, 1, TEST_TIMEOUT_MILLIS)), + assertEquals(Arrays.asList(expectedPrimitive(PRIMITIVE_CLICK, 1, TEST_TIMEOUT_MILLIS)), mVibratorProviders.get(2).getEffectSegments(vibration.id)); assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)), mVibratorProviders.get(3).getEffectSegments(vibration.id)); @@ -2205,7 +2218,7 @@ public class VibrationThreadTest { private void verifyCallbacksTriggered(HalVibration vibration, Status expectedStatus) { assertThat(vibration.getStatus()).isEqualTo(expectedStatus); - verify(mManagerHooks).onVibrationThreadReleased(vibration.id); + verify(mManagerHooks).onVibrationThreadReleased(eq(vibration.id)); } private static final class TestLooperAutoDispatcher extends Thread { diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java index 0978f48491cc..4df13deaed7d 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java @@ -203,9 +203,10 @@ public class VibratorControllerTest { @Test public void setAmplitude_vibratorVibrating_setsAmplitude() { - when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); + when(mNativeWrapperMock.on(anyLong(), anyLong(), anyLong())) + .thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); - controller.on(100, /* vibrationId= */ 1); + controller.on(100, 1, 1); assertTrue(controller.isVibrating()); assertEquals(-1, controller.getCurrentAmplitude(), /* delta= */ 0); @@ -215,81 +216,84 @@ public class VibratorControllerTest { @Test public void on_withDuration_turnsVibratorOn() { - when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); + when(mNativeWrapperMock.on(anyLong(), anyLong(), anyLong())) + .thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); - controller.on(100, 10); + controller.on(100, 10, 20); assertTrue(controller.isVibrating()); - verify(mNativeWrapperMock).on(eq(100L), eq(10L)); + verify(mNativeWrapperMock).on(eq(100L), eq(10L), eq(20L)); } @Test public void on_withPrebaked_performsEffect() { - when(mNativeWrapperMock.perform(anyLong(), anyLong(), anyLong())).thenReturn(10L); + when(mNativeWrapperMock.perform(anyLong(), anyLong(), anyLong(), anyLong())) + .thenReturn(10L); VibratorController controller = createController(); PrebakedSegment prebaked = createPrebaked(VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_STRENGTH_MEDIUM); - assertEquals(10L, controller.on(prebaked, 11)); + assertEquals(10L, controller.on(prebaked, 11, 23)); assertTrue(controller.isVibrating()); verify(mNativeWrapperMock).perform(eq((long) VibrationEffect.EFFECT_CLICK), - eq((long) VibrationEffect.EFFECT_STRENGTH_MEDIUM), eq(11L)); + eq((long) VibrationEffect.EFFECT_STRENGTH_MEDIUM), eq(11L), eq(23L)); } @Test public void on_withComposed_performsEffect() { mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); - when(mNativeWrapperMock.compose(any(), anyLong())).thenReturn(15L); + when(mNativeWrapperMock.compose(any(), anyLong(), anyLong())).thenReturn(15L); VibratorController controller = createController(); PrimitiveSegment[] primitives = new PrimitiveSegment[]{ new PrimitiveSegment(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10) }; - assertEquals(15L, controller.on(primitives, 12)); + assertEquals(15L, controller.on(primitives, 12, 34)); assertTrue(controller.isVibrating()); - verify(mNativeWrapperMock).compose(eq(primitives), eq(12L)); + verify(mNativeWrapperMock).compose(eq(primitives), eq(12L), eq(34L)); } @Test public void on_withComposedPwle_performsEffect() { mockVibratorCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS); - when(mNativeWrapperMock.composePwle(any(), anyInt(), anyLong())).thenReturn(15L); + when(mNativeWrapperMock.composePwle(any(), anyInt(), anyLong(), anyLong())).thenReturn(15L); VibratorController controller = createController(); RampSegment[] primitives = new RampSegment[]{ new RampSegment(/* startAmplitude= */ 0, /* endAmplitude= */ 1, /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 200, /* duration= */ 10) }; - assertEquals(15L, controller.on(primitives, 12)); + assertEquals(15L, controller.on(primitives, 12, 45)); assertTrue(controller.isVibrating()); - verify(mNativeWrapperMock).composePwle(eq(primitives), eq(Braking.NONE), eq(12L)); + verify(mNativeWrapperMock).composePwle(eq(primitives), eq(Braking.NONE), eq(12L), eq(45L)); } @Test public void on_withComposedPwleV2_performsEffect() { mockVibratorCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); - when(mNativeWrapperMock.composePwleV2(any(), anyLong())).thenReturn(15L); + when(mNativeWrapperMock.composePwleV2(any(), anyLong(), anyLong())).thenReturn(15L); VibratorController controller = createController(); PwlePoint[] primitives = new PwlePoint[]{ new PwlePoint(/*amplitude=*/ 0, /*frequencyHz=*/ 100, /*timeMillis=*/ 0), new PwlePoint(/*amplitude=*/ 1, /*frequencyHz=*/ 200, /*timeMillis=*/ 10) }; - assertEquals(15L, controller.on(primitives, 12)); + assertEquals(15L, controller.on(primitives, 12, 53)); assertTrue(controller.isVibrating()); - verify(mNativeWrapperMock).composePwleV2(eq(primitives), eq(12L)); + verify(mNativeWrapperMock).composePwleV2(eq(primitives), eq(12L), eq(53L)); } @Test public void off_turnsOffVibrator() { - when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); + when(mNativeWrapperMock.on(anyLong(), anyLong(), anyLong())) + .thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); - controller.on(100, 1); + controller.on(100, 1, 1); assertTrue(controller.isVibrating()); controller.off(); @@ -301,10 +305,11 @@ public class VibratorControllerTest { @Test public void reset_turnsOffVibratorAndDisablesExternalControl() { mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); - when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); + when(mNativeWrapperMock.on(anyLong(), anyLong(), anyLong())) + .thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); - controller.on(100, 1); + controller.on(100, 1, 1); assertTrue(controller.isVibrating()); controller.reset(); @@ -315,12 +320,13 @@ public class VibratorControllerTest { @Test public void registerVibratorStateListener_callbacksAreTriggered() throws Exception { - when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); + when(mNativeWrapperMock.on(anyLong(), anyLong(), anyLong())) + .thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); controller.registerVibratorStateListener(mVibratorStateListenerMock); - controller.on(10, 1); - controller.on(100, 2); + controller.on(10, 1, 1); + controller.on(100, 2, 1); controller.off(); controller.off(); @@ -334,19 +340,20 @@ public class VibratorControllerTest { @Test public void unregisterVibratorStateListener_callbackNotTriggeredAfter() throws Exception { - when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); + when(mNativeWrapperMock.on(anyLong(), anyLong(), anyLong())) + .thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); controller.registerVibratorStateListener(mVibratorStateListenerMock); verify(mVibratorStateListenerMock).onVibrating(false); - controller.on(10, 1); + controller.on(10, 1, 1); verify(mVibratorStateListenerMock).onVibrating(true); controller.unregisterVibratorStateListener(mVibratorStateListenerMock); Mockito.clearInvocations(mVibratorStateListenerMock); - controller.on(10, 1); + controller.on(10, 1, 1); verifyNoMoreInteractions(mVibratorStateListenerMock); } diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java index 3f3476716831..bd806b7f7f37 100644 --- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -118,11 +118,11 @@ public final class FakeVibratorControllerProvider { } @Override - public long on(long milliseconds, long vibrationId) { + public long on(long milliseconds, long vibrationId, long stepId) { recordEffectSegment(vibrationId, new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, /* frequencyHz= */ 0, (int) milliseconds)); applyLatency(mOnLatency); - scheduleListener(milliseconds, vibrationId); + scheduleListener(milliseconds, vibrationId, stepId); return milliseconds; } @@ -139,7 +139,7 @@ public final class FakeVibratorControllerProvider { } @Override - public long perform(long effect, long strength, long vibrationId) { + public long perform(long effect, long strength, long vibrationId, long stepId) { if (mSupportedEffects == null || Arrays.binarySearch(mSupportedEffects, (int) effect) < 0) { return 0; @@ -147,13 +147,13 @@ public final class FakeVibratorControllerProvider { recordEffectSegment(vibrationId, new PrebakedSegment((int) effect, false, (int) strength)); applyLatency(mOnLatency); - scheduleListener(EFFECT_DURATION, vibrationId); + scheduleListener(EFFECT_DURATION, vibrationId, stepId); return EFFECT_DURATION; } @Override public long performVendorEffect(Parcel vendorData, long strength, float scale, - float adaptiveScale, long vibrationId) { + float adaptiveScale, long vibrationId, long stepId) { if ((mCapabilities & IVibrator.CAP_PERFORM_VENDOR_EFFECTS) == 0) { return 0; } @@ -161,13 +161,13 @@ public final class FakeVibratorControllerProvider { recordVendorEffect(vibrationId, new VibrationEffect.VendorEffect(bundle, (int) strength, scale, adaptiveScale)); applyLatency(mOnLatency); - scheduleListener(mVendorEffectDuration, vibrationId); + scheduleListener(mVendorEffectDuration, vibrationId, stepId); // HAL has unknown duration for vendor effects. return Long.MAX_VALUE; } @Override - public long compose(PrimitiveSegment[] primitives, long vibrationId) { + public long compose(PrimitiveSegment[] primitives, long vibrationId, long stepId) { if (mSupportedPrimitives == null) { return 0; } @@ -182,12 +182,13 @@ public final class FakeVibratorControllerProvider { recordEffectSegment(vibrationId, primitive); } applyLatency(mOnLatency); - scheduleListener(duration, vibrationId); + scheduleListener(duration, vibrationId, stepId); return duration; } @Override - public long composePwle(RampSegment[] primitives, int braking, long vibrationId) { + public long composePwle(RampSegment[] primitives, int braking, long vibrationId, + long stepId) { long duration = 0; for (RampSegment primitive : primitives) { duration += primitive.getDuration(); @@ -195,19 +196,19 @@ public final class FakeVibratorControllerProvider { } recordBraking(vibrationId, braking); applyLatency(mOnLatency); - scheduleListener(duration, vibrationId); + scheduleListener(duration, vibrationId, stepId); return duration; } @Override - public long composePwleV2(PwlePoint[] pwlePoints, long vibrationId) { + public long composePwleV2(PwlePoint[] pwlePoints, long vibrationId, long stepId) { long duration = 0; for (PwlePoint pwlePoint: pwlePoints) { duration += pwlePoint.getTimeMillis(); recordEffectPwlePoint(vibrationId, pwlePoint); } applyLatency(mOnLatency); - scheduleListener(duration, vibrationId); + scheduleListener(duration, vibrationId, stepId); return duration; } @@ -263,8 +264,8 @@ public final class FakeVibratorControllerProvider { } } - private void scheduleListener(long vibrationDuration, long vibrationId) { - mHandler.postDelayed(() -> listener.onComplete(vibratorId, vibrationId), + private void scheduleListener(long vibrationDuration, long vibrationId, long stepId) { + mHandler.postDelayed(() -> listener.onComplete(vibratorId, vibrationId, stepId), vibrationDuration + mCompletionCallbackDelay); } } diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java index 8e2cea7a8c78..6d8a48799112 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java @@ -244,9 +244,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { {"Meta + L -> Lock Homescreen", new int[]{META_KEY, KeyEvent.KEYCODE_L}, KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, KeyEvent.KEYCODE_L, META_ON}, - {"Meta + Ctrl + N -> Open Notes", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_N}, - KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, KeyEvent.KEYCODE_N, - META_ON | CTRL_ON}, {"Meta + Ctrl + DPAD_DOWN -> Enter desktop mode", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_DOWN}, KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE, diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index 32a3b7f2c9cc..22c86eb3a9b8 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -272,6 +272,19 @@ public class PhoneWindowManagerTests { } @Test + public void powerPress_withoutDreamManagerInternal_doesNotCrash() { + when(mDisplayPolicy.isAwake()).thenReturn(true); + mDreamManagerInternal = null; + initPhoneWindowManager(); + + // Power button pressed. + int eventTime = 0; + mPhoneWindowManager.powerPress(eventTime, 1, 0); + + // verify no crash + } + + @Test public void powerPress_hubOrDreamOrSleep_hubAvailableLocks() { when(mDisplayPolicy.isAwake()).thenReturn(true); mContext.getTestablePermissions().setPermission(android.Manifest.permission.DEVICE_POWER, @@ -352,5 +365,10 @@ public class PhoneWindowManagerTests { WindowWakeUpPolicy getWindowWakeUpPolicy() { return mock(WindowWakeUpPolicy.class); } + + @Override + DreamManagerInternal getDreamManagerInternal() { + return mDreamManagerInternal; + } } } 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 6923deec1ebb..301a7544227f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -56,8 +56,6 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.window.StartingWindowInfo.TYPE_PARAMETER_LEGACY_SPLASH_SCREEN; @@ -921,12 +919,6 @@ public class ActivityRecordTests extends WindowTestsBase { // animation and AR#takeSceneTransitionInfo also clear the AR#mPendingOptions assertNull(activity.takeSceneTransitionInfo()); assertNull(activity.getOptions()); - - final AppTransition appTransition = activity.mDisplayContent.mAppTransition; - spyOn(appTransition); - activity.applyOptionsAnimation(); - - verify(appTransition).overridePendingAppTransitionRemote(any()); } @Test @@ -1009,6 +1001,8 @@ public class ActivityRecordTests extends WindowTestsBase { assertTrue(style.disablePreview()); assertTrue(style.optOutEdgeToEdge()); assertEquals(1 /* icon_preferred */, style.mSplashScreenBehavior); + assertEquals(style.noDisplay(), mAtm.mInternal.isNoDisplay(activity.packageName, + activity.info.theme, activity.mUserId)); } /** @@ -1188,7 +1182,6 @@ public class ActivityRecordTests extends WindowTestsBase { FINISH_RESULT_REQUESTED, activity.finishIfPossible("test", false /* oomAdj */)); assertEquals(PAUSING, activity.getState()); verify(activity).setVisibility(eq(false)); - verify(activity.mDisplayContent).prepareAppTransition(eq(TRANSIT_CLOSE)); } /** @@ -1235,7 +1228,6 @@ public class ActivityRecordTests extends WindowTestsBase { activity.finishIfPossible("test", false /* oomAdj */); verify(activity).setVisibility(eq(false)); - verify(activity.mDisplayContent).prepareAppTransition(eq(TRANSIT_CLOSE)); verify(activity.mDisplayContent, never()).executeAppTransition(); } @@ -1252,7 +1244,6 @@ public class ActivityRecordTests extends WindowTestsBase { activity.finishIfPossible("test", false /* oomAdj */); verify(activity, atLeast(1)).setVisibility(eq(false)); - verify(activity.mDisplayContent).prepareAppTransition(eq(TRANSIT_CLOSE)); verify(activity.mDisplayContent).executeAppTransition(); } @@ -1273,7 +1264,6 @@ public class ActivityRecordTests extends WindowTestsBase { activity.finishIfPossible("test", false /* oomAdj */); - verify(activity.mDisplayContent, never()).prepareAppTransition(eq(TRANSIT_CLOSE)); assertFalse(activity.inTransition()); // finishIfPossible -> completeFinishing -> addToFinishingAndWaitForIdle @@ -2655,10 +2645,6 @@ public class ActivityRecordTests extends WindowTestsBase { @Presubmit public void testGetOrientation() { mDisplayContent.setIgnoreOrientationRequest(false); - // ActivityBuilder will resume top activities and cause the activity been added into - // opening apps list. Since this test is focus on the effect of visible on getting - // orientation, we skip app transition to avoid interference. - doNothing().when(mDisplayContent).prepareAppTransition(anyInt()); final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); activity.setVisible(true); @@ -2923,7 +2909,6 @@ public class ActivityRecordTests extends WindowTestsBase { sources.add(activity2); doReturn(true).when(activity2).okToAnimate(); doReturn(true).when(activity2).isAnimating(); - assertTrue(activity2.applyAnimation(null, TRANSIT_OLD_ACTIVITY_OPEN, true, false, sources)); } @Test public void testTrackingStartingWindowThroughTrampoline() { diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index d5b9751b0f51..a99bc4966c03 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -37,7 +37,6 @@ import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; -import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowInsets.Type.statusBars; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; @@ -56,6 +55,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; +import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; @@ -63,9 +63,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE; import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; @@ -1742,8 +1739,6 @@ public class DisplayContentTests extends WindowTestsBase { public void testFixedRotationWithPip() { final DisplayContent displayContent = mDefaultDisplay; displayContent.setIgnoreOrientationRequest(false); - // Unblock the condition in PinnedTaskController#continueOrientationChangeIfNeeded. - doNothing().when(displayContent).prepareAppTransition(anyInt()); // Make resume-top really update the activity state. setBooted(mAtm); clearInvocations(mWm); @@ -1826,7 +1821,7 @@ public class DisplayContentTests extends WindowTestsBase { .setTask(nonTopVisible.getTask()).setVisible(false) .setActivityTheme(android.R.style.Theme_Translucent).build(); final TestTransitionPlayer player = registerTestTransitionPlayer(); - mDisplayContent.requestTransitionAndLegacyPrepare(WindowManager.TRANSIT_OPEN, 0); + mDisplayContent.requestTransitionAndLegacyPrepare(WindowManager.TRANSIT_OPEN, 0, null); translucentTop.setVisibility(true); mDisplayContent.updateOrientation(); assertEquals("Non-top visible activity must be portrait", @@ -2373,33 +2368,6 @@ public class DisplayContentTests extends WindowTestsBase { @SetupWindows(addWindows = W_INPUT_METHOD) @Test - public void testShowImeScreenshot() { - final Task rootTask = createTask(mDisplayContent); - final Task task = createTaskInRootTask(rootTask, 0 /* userId */); - final ActivityRecord activity = createActivityRecord(mDisplayContent, task); - final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken( - activity).build(); - task.getDisplayContent().prepareAppTransition(TRANSIT_CLOSE); - doReturn(true).when(task).okToAnimate(); - ArrayList<WindowContainer> sources = new ArrayList<>(); - sources.add(activity); - - mDisplayContent.setImeLayeringTarget(win); - spyOn(mDisplayContent); - - // Expecting the IME screenshot only be attached when performing task closing transition. - task.applyAnimation(null, TRANSIT_OLD_TASK_CLOSE, false /* enter */, - false /* isVoiceInteraction */, sources); - verify(mDisplayContent).showImeScreenshot(); - - clearInvocations(mDisplayContent); - activity.applyAnimation(null, TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE, false /* enter */, - false /* isVoiceInteraction */, sources); - verify(mDisplayContent, never()).showImeScreenshot(); - } - - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test public void testShowImeScreenshot_removeCurSnapshotBeforeCreateNext() { final Task rootTask = createTask(mDisplayContent); final Task task = createTaskInRootTask(rootTask, 0 /* userId */); @@ -2427,32 +2395,6 @@ public class DisplayContentTests extends WindowTestsBase { @UseTestDisplay(addWindows = {W_INPUT_METHOD}) @Test - public void testRemoveImeScreenshot_whenTargetSurfaceWasInvisible() { - final Task rootTask = createTask(mDisplayContent); - final Task task = createTaskInRootTask(rootTask, 0 /* userId */); - final ActivityRecord activity = createActivityRecord(mDisplayContent, task); - final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken( - activity).build(); - win.onSurfaceShownChanged(true); - makeWindowVisible(win, mDisplayContent.mInputMethodWindow); - task.getDisplayContent().prepareAppTransition(TRANSIT_CLOSE); - doReturn(true).when(task).okToAnimate(); - ArrayList<WindowContainer> sources = new ArrayList<>(); - sources.add(activity); - - mDisplayContent.setImeLayeringTarget(win); - mDisplayContent.setImeInputTarget(win); - mDisplayContent.getInsetsStateController().getImeSourceProvider().setImeShowing(true); - task.applyAnimation(null, TRANSIT_OLD_TASK_CLOSE, false /* enter */, - false /* isVoiceInteraction */, sources); - assertNotNull(mDisplayContent.mImeScreenshot); - - win.onSurfaceShownChanged(false); - assertNull(mDisplayContent.mImeScreenshot); - } - - @UseTestDisplay(addWindows = {W_INPUT_METHOD}) - @Test public void testRemoveImeScreenshot_whenWindowRemoveImmediately() { final Task rootTask = createTask(mDisplayContent); final Task task = createTaskInRootTask(rootTask, 0 /* userId */); @@ -2751,55 +2693,77 @@ public class DisplayContentTests extends WindowTestsBase { @SetupWindows(addWindows = W_INPUT_METHOD) @Test - public void testImeChildWindowFocusWhenImeLayeringTargetChanges() { - final WindowState imeChildWindow = newWindowBuilder("imeChildWindow", + public void testImeChildWindowFocusWhenImeParentWindowChanges() { + final var imeChildWindow = newWindowBuilder("imeChildWindow", TYPE_APPLICATION_ATTACHED_DIALOG).setParent(mImeWindow).build(); - makeWindowVisibleAndDrawn(imeChildWindow, mImeWindow); - assertTrue(imeChildWindow.canReceiveKeys()); - mDisplayContent.setInputMethodWindowLocked(mImeWindow); - - // Verify imeChildWindow can be focused window if the next IME target requests IME visible. - final WindowState imeAppTarget = newWindowBuilder("imeAppTarget", - TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build(); - mDisplayContent.setImeLayeringTarget(imeAppTarget); - spyOn(imeAppTarget); - doReturn(true).when(imeAppTarget).isRequestedVisible(ime()); - assertEquals(imeChildWindow, mDisplayContent.findFocusedWindow()); - - // Verify imeChildWindow doesn't be focused window if the next IME target does not - // request IME visible. - final WindowState nextImeAppTarget = newWindowBuilder("nextImeAppTarget", - TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build(); - mDisplayContent.setImeLayeringTarget(nextImeAppTarget); - assertNotEquals(imeChildWindow, mDisplayContent.findFocusedWindow()); + doTestImeWindowFocusWhenImeParentWindowChanged(imeChildWindow); } @SetupWindows(addWindows = W_INPUT_METHOD) @Test - public void testImeMenuDialogFocusWhenImeLayeringTargetChanges() { - final WindowState imeMenuDialog = newWindowBuilder("imeMenuDialog", + public void testImeDialogWindowFocusWhenImeParentWindowChanges() { + final var imeDialogWindow = newWindowBuilder("imeMenuDialog", TYPE_INPUT_METHOD_DIALOG).build(); - makeWindowVisibleAndDrawn(imeMenuDialog, mImeWindow); - assertTrue(imeMenuDialog.canReceiveKeys()); + doTestImeWindowFocusWhenImeParentWindowChanged(imeDialogWindow); + } + + @SetupWindows(addWindows = W_INPUT_METHOD) + @Test + public void testImeWindowFocusWhenImeParentWindowChanges() { + // Verify focusable, non-child IME windows. + final var otherImeWindow = newWindowBuilder("otherImeWindow", + TYPE_INPUT_METHOD).build(); + doTestImeWindowFocusWhenImeParentWindowChanged(otherImeWindow); + } + + private void doTestImeWindowFocusWhenImeParentWindowChanged(@NonNull WindowState window) { + makeWindowVisibleAndDrawn(window, mImeWindow); + assertTrue("Window canReceiveKeys", window.canReceiveKeys()); mDisplayContent.setInputMethodWindowLocked(mImeWindow); - // Verify imeMenuDialog can be focused window if the next IME target requests IME visible. + // Verify window can be focused if the IME parent is visible and the IME is visible. final WindowState imeAppTarget = newWindowBuilder("imeAppTarget", TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build(); mDisplayContent.setImeLayeringTarget(imeAppTarget); - imeAppTarget.setRequestedVisibleTypes(ime()); - assertEquals(imeMenuDialog, mDisplayContent.findFocusedWindow()); - - // Verify imeMenuDialog doesn't be focused window if the next IME target is closing. + mDisplayContent.updateImeInputAndControlTarget(imeAppTarget); + final var imeProvider = mDisplayContent.getInsetsStateController().getImeSourceProvider(); + imeProvider.setImeShowing(true); + final var imeParentWindow = mDisplayContent.getImeParentWindow(); + assertNotNull("IME parent window is not null", imeParentWindow); + assertTrue("IME parent window is visible", imeParentWindow.isVisibleRequested()); + assertTrue("IME is visible", imeProvider.isImeShowing()); + assertEquals("Window is the focused one", window, mDisplayContent.findFocusedWindow()); + + // Verify window can't be focused if the IME parent is not visible. final WindowState nextImeAppTarget = newWindowBuilder("nextImeAppTarget", TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build(); makeWindowVisibleAndDrawn(nextImeAppTarget); - // Even if the app still requests IME, the ime dialog should not gain focus if the target - // app is invisible. - nextImeAppTarget.setRequestedVisibleTypes(ime()); - nextImeAppTarget.mActivityRecord.setVisibility(false); + // Change layering target but keep input target (and thus imeParent) the same. mDisplayContent.setImeLayeringTarget(nextImeAppTarget); - assertNotEquals(imeMenuDialog, mDisplayContent.findFocusedWindow()); + // IME parent window is not visible, occluded by new layering target. + imeParentWindow.setVisibleRequested(false); + assertEquals("IME parent window did not change", imeParentWindow, + mDisplayContent.getImeParentWindow()); + assertFalse("IME parent window is not visible", imeParentWindow.isVisibleRequested()); + assertTrue("IME is visible", imeProvider.isImeShowing()); + assertNotEquals("Window is not the focused one when imeParent is not visible", window, + mDisplayContent.findFocusedWindow()); + + // Verify window can be focused if the IME is not visible. + mDisplayContent.updateImeInputAndControlTarget(nextImeAppTarget); + imeProvider.setImeShowing(false); + final var nextImeParentWindow = mDisplayContent.getImeParentWindow(); + assertNotNull("Next IME parent window is not null", nextImeParentWindow); + assertNotEquals("IME parent window changed", imeParentWindow, nextImeParentWindow); + assertTrue("Next IME parent window is visible", nextImeParentWindow.isVisibleRequested()); + assertFalse("IME is not visible", imeProvider.isImeShowing()); + if (window.isChildWindow()) { + assertNotEquals("Child window is not the focused on when the IME is not visible", + window, mDisplayContent.findFocusedWindow()); + } else { + assertEquals("Window is the focused one when the IME is not visible", + window, mDisplayContent.findFocusedWindow()); + } } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java index db90c28ec7df..2d4101e40615 100644 --- a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java @@ -17,17 +17,21 @@ package com.android.server.wm; import static android.view.Display.FLAG_PRESENTATION; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; +import android.annotation.NonNull; import android.graphics.Rect; import android.os.UserHandle; -import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.DisplayInfo; @@ -41,6 +45,7 @@ import android.view.WindowManagerGlobal; import androidx.test.filters.SmallTest; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,35 +58,100 @@ import org.junit.runner.RunWith; @RunWith(WindowTestRunner.class) public class PresentationControllerTests extends WindowTestsBase { + TestTransitionPlayer mPlayer; + + @Before + public void setUp() { + mPlayer = registerTestTransitionPlayer(); + } + @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) @Test - public void testPresentationHidesActivitiesBehind() { - final DisplayInfo displayInfo = new DisplayInfo(); - displayInfo.copyFrom(mDisplayInfo); - displayInfo.flags = FLAG_PRESENTATION; - final DisplayContent dc = createNewDisplay(displayInfo); - final int displayId = dc.getDisplayId(); - doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); + public void testPresentationShowAndHide() { + final DisplayContent dc = createPresentationDisplay(); final ActivityRecord activity = createActivityRecord(createTask(dc)); assertTrue(activity.isVisible()); - doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); - final int uid = 100000; // uid for non-system user + // Add a presentation window, which requests the activity to stop. + final WindowState window = addPresentationWindow(100000, dc.mDisplayId); + assertFalse(activity.isVisibleRequested()); + assertTrue(activity.isVisible()); + final Transition addTransition = window.mTransitionController.getCollectingTransition(); + assertEquals(TRANSIT_OPEN, addTransition.mType); + assertTrue(addTransition.isInTransition(window)); + assertTrue(addTransition.isInTransition(activity)); + + // Completing the transition makes the activity invisible. + completeTransition(addTransition, /*abortSync=*/ true); + assertFalse(activity.isVisible()); + + // Remove a Presentation window, which requests the activity to be resumed back. + window.removeIfPossible(); + final Transition removeTransition = window.mTransitionController.getCollectingTransition(); + assertEquals(TRANSIT_CLOSE, removeTransition.mType); + assertTrue(removeTransition.isInTransition(window)); + assertTrue(removeTransition.isInTransition(activity)); + assertTrue(activity.isVisibleRequested()); + assertFalse(activity.isVisible()); + + // Completing the transition makes the activity visible. + completeTransition(removeTransition, /*abortSync=*/ false); + assertTrue(activity.isVisible()); + } + + @DisableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) + @Test + public void testPresentationShowAndHide_flagDisabled() { + final DisplayContent dc = createPresentationDisplay(); + final ActivityRecord activity = createActivityRecord(createTask(dc)); + assertTrue(activity.isVisible()); + + final WindowState window = addPresentationWindow(100000, dc.mDisplayId); + assertFalse(window.mTransitionController.isCollecting()); + assertTrue(activity.isVisibleRequested()); + assertTrue(activity.isVisible()); + + window.removeIfPossible(); + assertFalse(window.mTransitionController.isCollecting()); + assertTrue(activity.isVisibleRequested()); + assertTrue(activity.isVisible()); + assertFalse(window.isAttached()); + } + + private WindowState addPresentationWindow(int uid, int displayId) { final Session session = createTestSession(mAtm, 1234 /* pid */, uid); final int userId = UserHandle.getUserId(uid); - doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); + doReturn(true).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); final WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.TYPE_PRESENTATION); - final IWindow clientWindow = new TestIWindow(); - final int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, + final int res = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(), new InsetsSourceControl.Array(), new Rect(), new float[1]); - assertTrue(result >= WindowManagerGlobal.ADD_OKAY); - assertFalse(activity.isVisible()); - + assertTrue(res >= WindowManagerGlobal.ADD_OKAY); final WindowState window = mWm.windowForClientLocked(session, clientWindow, false); - window.removeImmediately(); - assertTrue(activity.isVisible()); + window.mHasSurface = true; + return window; + } + + private DisplayContent createPresentationDisplay() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.copyFrom(mDisplayInfo); + displayInfo.flags = FLAG_PRESENTATION; + final DisplayContent dc = createNewDisplay(displayInfo); + final int displayId = dc.getDisplayId(); + doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); + return dc; + } + + private void completeTransition(@NonNull Transition transition, boolean abortSync) { + final ActionChain chain = ActionChain.testFinish(transition); + if (abortSync) { + // Forcefully finishing the active sync for testing purpose. + mWm.mSyncEngine.abort(transition.getSyncId()); + } else { + transition.onTransactionReady(transition.getSyncId(), mTransaction); + } + transition.finishTransition(chain); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index 59335d3bb135..3776b03695d5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -98,6 +98,7 @@ import com.android.server.policy.PermissionPolicyInternal; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.testutils.StubTransaction; import com.android.server.uri.UriGrantsManagerInternal; +import com.android.window.flags.Flags; import org.junit.rules.TestRule; import org.junit.runner.Description; @@ -263,7 +264,7 @@ public class SystemServicesTestRule implements TestRule { final DisplayManagerGlobal dmg = DisplayManagerGlobal.getInstance(); spyOn(dmg); doNothing().when(dmg).registerDisplayListener( - any(), any(Executor.class), anyLong(), anyString()); + any(), any(Executor.class), anyLong(), anyString(), anyBoolean()); doNothing().when(dmg).registerTopologyListener(any(Executor.class), any(), anyString()); } @@ -657,6 +658,13 @@ public class SystemServicesTestRule implements TestRule { AppWarnings appWarnings = getAppWarningsLocked(); spyOn(appWarnings); doNothing().when(appWarnings).onStartActivity(any()); + + if (Flags.trackSystemUiContextBeforeWms()) { + final Context uiContext = getUiContext(); + spyOn(uiContext); + doNothing().when(uiContext).registerComponentCallbacks(any()); + doNothing().when(uiContext).unregisterComponentCallbacks(any()); + } } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java index ccce57a81e41..b1525cf00dc6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java @@ -30,6 +30,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; import android.content.res.Configuration; import android.graphics.Insets; import android.graphics.Rect; @@ -39,6 +40,7 @@ import android.view.DisplayCutout; import android.view.DisplayInfo; import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry; +import com.android.window.flags.Flags; class TestDisplayContent extends DisplayContent { @@ -79,6 +81,13 @@ class TestDisplayContent extends DisplayContent { WindowTestsBase.suppressInsetsAnimation(insetsPolicy.getPermanentControlTarget()); WindowTestsBase.suppressInsetsAnimation(insetsPolicy.getTransientControlTarget()); + if (Flags.trackSystemUiContextBeforeWms()) { + final Context uiContext = getDisplayUiContext(); + spyOn(uiContext); + doNothing().when(uiContext).registerComponentCallbacks(any()); + doNothing().when(uiContext).unregisterComponentCallbacks(any()); + } + // For devices that set the sysprop ro.bootanim.set_orientation_<display_id> // See DisplayRotation#readDefaultDisplayRotation for context. // Without that, meaning of height and width in context of the tests can be swapped if diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 2ee34d3a4b36..77ad7f7bac7f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -683,7 +683,7 @@ public class TransitionTests extends WindowTestsBase { app.onStartingWindowDrawn(); // The task appeared event should be deferred until transition ready. assertFalse(task.taskAppearedReady()); - testPlayer.onTransactionReady(app.getSyncTransaction()); + testPlayer.onTransactionReady(); assertTrue(task.taskAppearedReady()); assertTrue(playerProc.isRunningRemoteTransition()); assertTrue(controller.mRemotePlayer.reportRunning(delegateProc.getThread())); @@ -1162,7 +1162,8 @@ public class TransitionTests extends WindowTestsBase { screenDecor.updateSurfacePosition(mMockT); assertEquals(prevPos, screenDecor.mLastSurfacePosition); - final SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction startTransaction = mTransaction; + clearInvocations(startTransaction); final SurfaceControl.TransactionCommittedListener transactionCommittedListener = onRotationTransactionReady(player, startTransaction); @@ -1213,7 +1214,8 @@ public class TransitionTests extends WindowTestsBase { assertFalse(statusBar.mToken.inTransition()); assertTrue(app.getTask().inTransition()); - final SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction startTransaction = mTransaction; + clearInvocations(startTransaction); final SurfaceControl leash = statusBar.mToken.getAnimationLeash(); doReturn(true).when(leash).isValid(); final SurfaceControl.TransactionCommittedListener transactionCommittedListener = @@ -1287,7 +1289,8 @@ public class TransitionTests extends WindowTestsBase { // Avoid DeviceStateController disturbing the test by triggering another rotation change. doReturn(false).when(mDisplayContent).updateRotationUnchecked(); - onRotationTransactionReady(player, mWm.mTransactionFactory.get()).onTransactionCommitted(); + clearInvocations(mTransaction); + onRotationTransactionReady(player, mTransaction).onTransactionCommitted(); assertEquals(ROTATION_ANIMATION_SEAMLESS, player.mLastReady.getChange( mDisplayContent.mRemoteToken.toWindowContainerToken()).getRotationAnimation()); spyOn(navBarInsetsProvider); @@ -1350,7 +1353,7 @@ public class TransitionTests extends WindowTestsBase { mDisplayContent.setFixedRotationLaunchingAppUnchecked(home); doReturn(true).when(home).hasFixedRotationTransform(any()); player.startTransition(); - player.onTransactionReady(mDisplayContent.getSyncTransaction()); + player.onTransactionReady(); final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation(); final RemoteDisplayChangeController displayChangeController = mDisplayContent @@ -3071,8 +3074,11 @@ public class TransitionTests extends WindowTestsBase { TestTransitionPlayer player, SurfaceControl.Transaction startTransaction) { final ArgumentCaptor<SurfaceControl.TransactionCommittedListener> listenerCaptor = ArgumentCaptor.forClass(SurfaceControl.TransactionCommittedListener.class); - player.onTransactionReady(startTransaction); - verify(startTransaction).addTransactionCommittedListener(any(), listenerCaptor.capture()); + player.onTransactionReady(); + // The startTransaction is from mWm.mTransactionFactory.get() in SyncGroup#finishNow. + // 2 times are from SyncGroup#finishNow and AsyncRotationController#setupStartTransaction. + verify(startTransaction, times(2)).addTransactionCommittedListener( + any(), listenerCaptor.capture()); return listenerCaptor.getValue(); } } 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 cee98fb1b34c..4f310de1e48b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -30,8 +30,6 @@ import static android.view.WindowInsets.Type.statusBars; 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; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; @@ -1121,7 +1119,6 @@ public class WindowContainerTests extends WindowTestsBase { final ActivityRecord activity = createActivityRecord(mDisplayContent, task); final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken( activity).build(); - task.getDisplayContent().prepareAppTransition(TRANSIT_CLOSE); spyOn(win); doReturn(true).when(task).okToAnimate(); ArrayList<WindowContainer> sources = new ArrayList<>(); @@ -1130,8 +1127,6 @@ public class WindowContainerTests extends WindowTestsBase { // Simulate the task applying the exit transition, verify the main window of the task // will be set the frozen insets state before the animation starts activity.setVisibility(false); - task.applyAnimation(null, TRANSIT_OLD_TASK_CLOSE, false /* enter */, - false /* isVoiceInteraction */, sources); verify(win).freezeInsetsState(); // Simulate the task transition finished. diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 7f9e591ca5e3..c6416850c464 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -861,11 +861,9 @@ public class WindowTestsBase extends SystemServiceTestsBase { /** Creates a {@link DisplayContent} and adds it to the system. */ private DisplayContent createNewDisplay(DisplayInfo info, @DisplayImePolicy int imePolicy, @Nullable SettingsEntry overrideSettings) { - final DisplayContent display = - new TestDisplayContent.Builder(mAtm, info) - .setOverrideSettings(overrideSettings) - .build(); - final DisplayContent dc = display.mDisplayContent; + final DisplayContent dc = new TestDisplayContent.Builder(mAtm, info) + .setOverrideSettings(overrideSettings) + .build(); // this display can show IME. dc.mWmService.mDisplayWindowSettings.setDisplayImePolicy(dc, imePolicy); return dc; @@ -2161,13 +2159,14 @@ public class WindowTestsBase extends SystemServiceTestsBase { mOrganizer.startTransition(mLastTransit.getToken(), null); } - void onTransactionReady(SurfaceControl.Transaction t) { - mLastTransit.onTransactionReady(mLastTransit.getSyncId(), t); + void onTransactionReady() { + // SyncGroup#finishNow -> Transition#onTransactionReady. + mController.mSyncEngine.abort(mLastTransit.getSyncId()); } void start() { startTransition(); - onTransactionReady(mock(SurfaceControl.Transaction.class)); + onTransactionReady(); } public void finish() { diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java index 49ad46131b0d..df43ed973fe7 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java @@ -24,7 +24,6 @@ import android.telephony.TelephonyManager; import android.util.Slog; import com.android.internal.annotations.GuardedBy; -import com.android.internal.telephony.flags.Flags; import java.util.ArrayList; import java.util.List; @@ -119,28 +118,18 @@ public class PhoneCallStateHandler { private boolean checkCallStatus() { List<SubscriptionInfo> infoList = mSubscriptionManager.getActiveSubscriptionInfoList(); if (infoList == null) return false; - if (!Flags.enforceTelephonyFeatureMapping()) { - return infoList.stream() - .filter(s -> (s.getSubscriptionId() - != SubscriptionManager.INVALID_SUBSCRIPTION_ID)) - .anyMatch(s -> isCallOngoingFromState( - mTelephonyManager - .createForSubscriptionId(s.getSubscriptionId()) - .getCallStateForSubscription())); - } else { - return infoList.stream() - .filter(s -> (s.getSubscriptionId() - != SubscriptionManager.INVALID_SUBSCRIPTION_ID)) - .anyMatch(s -> { - try { - return isCallOngoingFromState(mTelephonyManager - .createForSubscriptionId(s.getSubscriptionId()) - .getCallStateForSubscription()); - } catch (UnsupportedOperationException e) { - return false; - } - }); - } + return infoList.stream() + .filter(s -> (s.getSubscriptionId() + != SubscriptionManager.INVALID_SUBSCRIPTION_ID)) + .anyMatch(s -> { + try { + return isCallOngoingFromState(mTelephonyManager + .createForSubscriptionId(s.getSubscriptionId()) + .getCallStateForSubscription()); + } catch (UnsupportedOperationException e) { + return false; + } + }); } private void updateTelephonyListeners() { diff --git a/startop/OWNERS b/startop/OWNERS index 11d5ad0f000a..3414a7469ac2 100644 --- a/startop/OWNERS +++ b/startop/OWNERS @@ -1,2 +1 @@ include platform/art:/OWNERS -keunyoung@google.com diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/flicker/LaunchTaskPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/flicker/LaunchTaskPortrait.kt new file mode 100644 index 000000000000..c82ce8a4cb1d --- /dev/null +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/flicker/LaunchTaskPortrait.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 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.flicker.service.transitions.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.service.transitions.scenarios.LaunchTask +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class LaunchTaskPortrait : LaunchTask(Rotation.ROTATION_0) { + @ExpectedScenarios(["TASK_TRANSITION_SCENARIO", "OPEN_NEW_TASK_APP_SCENARIO"]) + @Test + override fun openNewTask() = super.openNewTask() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(TASK_TRANSITION_SCENARIO) + .use(OPEN_NEW_TASK_APP_SCENARIO) + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/scenarios/LaunchTask.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/scenarios/LaunchTask.kt new file mode 100644 index 000000000000..147477d728be --- /dev/null +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/scenarios/LaunchTask.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2025 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.flicker.service.transitions.scenarios + +import android.app.Instrumentation +import android.tools.Rotation +import android.tools.flicker.AssertionInvocationGroup +import android.tools.flicker.assertors.assertions.AppWindowCoversFullScreenAtStart +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart +import android.tools.flicker.assertors.assertions.BackgroundShowsInTransition +import android.tools.flicker.assertors.assertions.LayerBecomesInvisible +import android.tools.flicker.assertors.assertions.LayerBecomesVisible +import android.tools.flicker.assertors.assertions.LayerIsNeverVisible +import android.tools.flicker.assertors.assertions.AppWindowIsNeverVisible +import android.tools.flicker.config.AssertionTemplates +import android.tools.flicker.config.FlickerConfigEntry +import android.tools.flicker.config.ScenarioId +import android.tools.flicker.config.appclose.Components.CLOSING_APPS +import android.tools.flicker.config.appclose.Components.CLOSING_CHANGES +import android.tools.flicker.config.applaunch.Components.OPENING_CHANGES +import android.tools.flicker.config.common.Components.LAUNCHER +import android.tools.flicker.config.common.Components.WALLPAPER +import android.tools.flicker.extractors.TaggedCujTransitionMatcher +import android.tools.flicker.extractors.TaggedScenarioExtractorBuilder +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import android.tools.traces.events.CujType +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.NewTasksAppHelper +import org.junit.After +import org.junit.Before +import org.junit.Test + + +/** + * This tests performs a transition between tasks + */ +abstract class LaunchTask(val rotation: Rotation = Rotation.ROTATION_0) { + protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val tapl = LauncherInstrumentation() + + private val launchNewTaskApp = NewTasksAppHelper(instrumentation) + + @Before + fun setup() { + tapl.setEnableRotation(true) + ChangeDisplayOrientationRule.setRotation(rotation) + tapl.setExpectedRotation(rotation.value) + launchNewTaskApp.launchViaIntent(wmHelper) + } + + @Test + open fun openNewTask() { + launchNewTaskApp.openNewTask(device, wmHelper) + } + + @After + fun tearDown() { + launchNewTaskApp.exit(wmHelper) + } + + companion object { + /** + * General task transition scenario that can be reused for any trace + */ + val TASK_TRANSITION_SCENARIO = + FlickerConfigEntry( + scenarioId = ScenarioId("TASK_TRANSITION_SCENARIO"), + extractor = TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DEFAULT_TASK_TO_TASK_ANIMATION) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = true) + ) + .build(), + assertions = listOf( + // Opening changes replace the closing ones + LayerBecomesInvisible(CLOSING_CHANGES), + AppWindowOnTopAtStart(CLOSING_CHANGES), + LayerBecomesVisible(OPENING_CHANGES), + AppWindowOnTopAtEnd(OPENING_CHANGES), + + // There is a background color and it's covering the transition area + BackgroundShowsInTransition(CLOSING_CHANGES) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) + ) + + /** + * Scenario that is making assertions that are valid for the new task app but that do not + * apply to other task transitions in general + */ + val OPEN_NEW_TASK_APP_SCENARIO = + FlickerConfigEntry( + scenarioId = ScenarioId("OPEN_NEW_TASK_APP_SCENARIO"), + extractor = TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DEFAULT_TASK_TO_TASK_ANIMATION) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = true) + ) + .build(), + assertions = AssertionTemplates.COMMON_ASSERTIONS + + listOf( + // Wallpaper and launcher never visible + LayerIsNeverVisible(WALLPAPER, mustExist = true), + LayerIsNeverVisible(LAUNCHER, mustExist = true), + AppWindowIsNeverVisible(LAUNCHER, mustExist = true), + // App window covers the display at start + AppWindowCoversFullScreenAtStart(CLOSING_APPS) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) + ) + } +}
\ No newline at end of file diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index a2f6f0051116..eac426700ec1 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -160,7 +160,7 @@ class InputManagerServiceTests { testLooper = TestLooper() service = InputManagerService(object : InputManagerService.Injector( - context, testLooper.looper, uEventManager) { + context, testLooper.looper, testLooper.looper, uEventManager) { override fun getNativeService( service: InputManagerService? ): NativeInputManagerService { diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 88e84966634b..c666fb7e05f1 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -207,7 +207,7 @@ class KeyGestureControllerTests { private fun setupKeyGestureController() { keyGestureController = - KeyGestureController(context, testLooper.looper, inputDataStore) + KeyGestureController(context, testLooper.looper, testLooper.looper, inputDataStore) Mockito.`when`(iInputManager.getAppLaunchBookmarks()) .thenReturn(keyGestureController.appLaunchBookmarks) keyGestureController.systemRunning() @@ -371,18 +371,6 @@ class KeyGestureControllerTests { intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( - "META + CTRL + N -> Open Notes", - intArrayOf( - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_N - ), - KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, - intArrayOf(KeyEvent.KEYCODE_N), - KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) - ), - TestData( "META + S -> Take Screenshot", intArrayOf( KeyEvent.KEYCODE_META_LEFT, diff --git a/tests/SoundTriggerTestApp/OWNERS b/tests/SoundTriggerTestApp/OWNERS index a0fcfc52704d..1e41886fe716 100644 --- a/tests/SoundTriggerTestApp/OWNERS +++ b/tests/SoundTriggerTestApp/OWNERS @@ -1,2 +1 @@ include /media/java/android/media/soundtrigger/OWNERS -mdooley@google.com diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java index be0c7daebb57..e62a89b17927 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java @@ -19,6 +19,7 @@ package com.android.internal.protolog; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.endsWith; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.times; @@ -157,13 +158,15 @@ public class ProtoLogCommandHandlerTest { cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "enable", "MY_GROUP" }); - Mockito.verify(mProtoLogConfigurationService).enableProtoLogToLogcat("MY_GROUP"); + Mockito.verify(mProtoLogConfigurationService) + .enableProtoLogToLogcat(Mockito.any(), eq("MY_GROUP")); cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "enable", "MY_GROUP", "MY_OTHER_GROUP" }); Mockito.verify(mProtoLogConfigurationService) - .enableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP"); + .enableProtoLogToLogcat(Mockito.any(), + eq("MY_GROUP"), eq("MY_OTHER_GROUP")); } @Test @@ -173,13 +176,15 @@ public class ProtoLogCommandHandlerTest { cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "disable", "MY_GROUP" }); - Mockito.verify(mProtoLogConfigurationService).disableProtoLogToLogcat("MY_GROUP"); + Mockito.verify(mProtoLogConfigurationService) + .disableProtoLogToLogcat(Mockito.any(), eq("MY_GROUP")); cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "disable", "MY_GROUP", "MY_OTHER_GROUP" }); Mockito.verify(mProtoLogConfigurationService) - .disableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP"); + .disableProtoLogToLogcat(Mockito.any(), + eq("MY_GROUP"), eq("MY_OTHER_GROUP")); } @Test diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java index 3be725101252..1f3f91ebf557 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java @@ -62,6 +62,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.PrintWriter; import java.util.List; /** @@ -234,7 +235,7 @@ public class ProtoLogConfigurationServiceTest { service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); - service.enableProtoLogToLogcat(TEST_GROUP); + service.enableProtoLogToLogcat(Mockito.mock(PrintWriter.class), TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); Mockito.verify(mMockClient).toggleLogcat(eq(true), @@ -251,7 +252,7 @@ public class ProtoLogConfigurationServiceTest { service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); - service.disableProtoLogToLogcat(TEST_GROUP); + service.disableProtoLogToLogcat(Mockito.mock(PrintWriter.class), TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); Mockito.verify(mMockClient).toggleLogcat(eq(false), @@ -269,7 +270,7 @@ public class ProtoLogConfigurationServiceTest { service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); - service.enableProtoLogToLogcat(OTHER_TEST_GROUP); + service.enableProtoLogToLogcat(Mockito.mock(PrintWriter.class), OTHER_TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); Mockito.verify(mMockClient, never()).toggleLogcat(anyBoolean(), any()); @@ -280,7 +281,7 @@ public class ProtoLogConfigurationServiceTest { final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); Truth.assertThat(service.getGroups()).asList().doesNotContain(TEST_GROUP); - service.enableProtoLogToLogcat(TEST_GROUP); + service.enableProtoLogToLogcat(Mockito.mock(PrintWriter.class), TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); final RegisterClientArgs args = new RegisterClientArgs(); diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 1273826c8ef8..8cd89ce89e83 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -273,7 +273,7 @@ public class TestableLooper { messages.add(message); } - // Repost all Messages back to the queuewith a new time. + // Repost all Messages back to the queue with a new time. while (true) { Message message = messages.poll(); if (message == null) { diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index ff4d8ef2ec25..0a5cb1ff4956 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -2649,6 +2649,10 @@ int LinkCommand::Action(const std::vector<std::string>& args) { ".mpg", ".mpeg", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2", ".wma", ".wmv", ".webm", ".mkv"}); + if (options_.no_compress_fonts) { + options_.extensions_to_not_compress.insert({".ttf", ".otf", ".ttc"}); + } + // Turn off auto versioning for static-libs. if (context.GetPackageType() == PackageType::kStaticLib) { options_.no_auto_version = true; diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 2f17853718ec..977978834fcd 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -78,6 +78,7 @@ struct LinkOptions { bool use_sparse_encoding = false; std::unordered_set<std::string> extensions_to_not_compress; std::optional<std::regex> regex_to_not_compress; + bool no_compress_fonts = false; FeatureFlagValues feature_flag_values; // Static lib options. @@ -300,6 +301,14 @@ class LinkCommand : public Command { "use the '$' symbol for end of line. Uses a case-sensitive ECMAScript" "regular expression grammar.", &no_compress_regex); + AddOptionalSwitch("--no-compress-fonts", + "Do not compress files with common extensions for fonts.\n" + "This allows loading fonts directly from the APK, without needing to\n" + "decompress them first. Loading fonts will be faster and use less memory.\n" + "The downside is that the APK will be larger.\n" + "Passing this flag is functionally equivalent to passing the following flags:\n" + "-0 .ttf -0 .otf -0 .ttc", + &options_.no_compress_fonts); AddOptionalSwitch("--warn-manifest-validation", "Treat manifest validation errors as warnings.", &options_.manifest_fixer_options.warn_validation); diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp index a2dc8f8ce0fd..41f8e250efd7 100644 --- a/tools/aapt2/cmd/Link_test.cpp +++ b/tools/aapt2/cmd/Link_test.cpp @@ -98,6 +98,7 @@ TEST_F(LinkTest, NoCompressAssets) { WriteFile(GetTestPath("assets/test.txt"), content); WriteFile(GetTestPath("assets/test.hello.txt"), content); WriteFile(GetTestPath("assets/test.hello.xml"), content); + WriteFile(GetTestPath("assets/fonts/myfont.ttf"), content); const std::string out_apk = GetTestPath("out.apk"); std::vector<std::string> link_args = { @@ -136,6 +137,10 @@ TEST_F(LinkTest, NoCompressAssets) { file = zip->FindFile("assets/test.hello.xml"); ASSERT_THAT(file, Ne(nullptr)); EXPECT_FALSE(file->WasCompressed()); + + file = zip->FindFile("assets/fonts/myfont.ttf"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_TRUE(file->WasCompressed()); } TEST_F(LinkTest, NoCompressResources) { @@ -182,6 +187,42 @@ TEST_F(LinkTest, NoCompressResources) { EXPECT_FALSE(file->WasCompressed()); } +TEST_F(LinkTest, NoCompressFonts) { + StdErrDiagnostics diag; + std::string content(500, 'a'); + const std::string compiled_files_dir = GetTestPath("compiled"); + ASSERT_TRUE(CompileFile(GetTestPath("res/raw/test.txt"), content, compiled_files_dir, &diag)); + WriteFile(GetTestPath("assets/fonts/myfont1.ttf"), content); + WriteFile(GetTestPath("assets/fonts/myfont2.ttf"), content); + + const std::string out_apk = GetTestPath("out.apk"); + std::vector<std::string> link_args = { + "--manifest", GetDefaultManifest(), + "-o", out_apk, + "--no-compress-fonts", + "-A", GetTestPath("assets") + }; + + ASSERT_TRUE(Link(link_args, compiled_files_dir, &diag)); + + std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(out_apk, &diag); + ASSERT_THAT(apk, Ne(nullptr)); + io::IFileCollection* zip = apk->GetFileCollection(); + ASSERT_THAT(zip, Ne(nullptr)); + + auto file = zip->FindFile("res/raw/test.txt"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_TRUE(file->WasCompressed()); + + file = zip->FindFile("assets/fonts/myfont1.ttf"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); + + file = zip->FindFile("assets/fonts/myfont2.ttf"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); +} + TEST_F(LinkTest, OverlayStyles) { StdErrDiagnostics diag; const std::string compiled_files_dir = GetTestPath("compiled"); diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 5c3dfdcadfec..6bdbaaed9858 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -3,6 +3,8 @@ ## Version 2.20 - Too many features, bug fixes, and improvements to list since the last minor version update in 2017. This README will be updated more frequently in the future. +- Added a new flag `--no-compress-fonts`. This can significantly speed up loading fonts from APK + assets, at the cost of increasing the storage size of the APK. ## Version 2.19 - Added navigation resource type. |