diff options
115 files changed, 3617 insertions, 992 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index 591e8ba859fc..4becc6b7c44a 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1008,13 +1008,21 @@ public class JobSchedulerService extends com.android.server.SystemService } @Override - public void onUserUnlocked(@NonNull TargetUser user) { + public void onUserStarting(@NonNull TargetUser user) { synchronized (mLock) { - // Note that the user has started after its unlocked instead of when the user - // actually starts because the storage won't be decrypted until unlock. mStartedUsers = ArrayUtils.appendInt(mStartedUsers, user.getUserIdentifier()); } - // Let's kick any outstanding jobs for this user. + // The user is starting but credential encrypted storage is still locked. + // Only direct-boot-aware jobs can safely run. + // Let's kick off any eligible jobs for this user. + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + + @Override + public void onUserUnlocked(@NonNull TargetUser user) { + // The user is fully unlocked and credential encrypted storage is now decrypted. + // Direct-boot-UNaware jobs can now safely run. + // Let's kick off any outstanding jobs for this user. mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); } diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 73961ff373dd..d8f20391098c 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -1288,6 +1288,23 @@ public final class DisplayManager { */ String KEY_FIXED_REFRESH_RATE_HIGH_AMBIENT_BRIGHTNESS_THRESHOLDS = "fixed_refresh_rate_high_ambient_brightness_thresholds"; + + /** + * Key for refresh rate when the device is in high brightness mode for sunlight visility. + * + * @see android.provider.DeviceConfig#NAMESPACE_DISPLAY_MANAGER + * @see android.R.integer#config_defaultRefreshRateInHbmSunlight + */ + String KEY_REFRESH_RATE_IN_HBM_SUNLIGHT = "refresh_rate_in_hbm_sunlight"; + + /** + * Key for refresh rate when the device is in high brightness mode for HDR. + * + * @see android.provider.DeviceConfig#NAMESPACE_DISPLAY_MANAGER + * @see android.R.integer#config_defaultRefreshRateInHbmHdr + */ + String KEY_REFRESH_RATE_IN_HBM_HDR = "refresh_rate_in_hbm_hdr"; + /** * Key for default peak refresh rate * diff --git a/core/java/android/util/MathUtils.java b/core/java/android/util/MathUtils.java index 971e16185815..aecde4415117 100644 --- a/core/java/android/util/MathUtils.java +++ b/core/java/android/util/MathUtils.java @@ -165,6 +165,10 @@ public final class MathUtils { return start + (stop - start) * amount; } + public static float lerp(int start, int stop, float amount) { + return lerp((float) start, (float) stop, amount); + } + /** * Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link * #lerp}{@code (a, b, s)} diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index aae6f5016891..f8eb95cbd48c 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -59,6 +59,7 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_ROW_SWIPE; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_SCROLL_FLING; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION; import android.annotation.IntDef; @@ -171,6 +172,7 @@ public class InteractionJankMonitor { public static final int CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP = 34; public static final int CUJ_PIP_TRANSITION = 35; public static final int CUJ_WALLPAPER_TRANSITION = 36; + public static final int CUJ_USER_SWITCH = 37; private static final int NO_STATSD_LOGGING = -1; @@ -216,6 +218,7 @@ public class InteractionJankMonitor { UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH, }; private static volatile InteractionJankMonitor sInstance; @@ -272,6 +275,7 @@ public class InteractionJankMonitor { CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP, CUJ_PIP_TRANSITION, CUJ_WALLPAPER_TRANSITION, + CUJ_USER_SWITCH, }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -642,6 +646,8 @@ public class InteractionJankMonitor { return "PIP_TRANSITION"; case CUJ_WALLPAPER_TRANSITION: return "WALLPAPER_TRANSITION"; + case CUJ_USER_SWITCH: + return "USER_SWITCH"; } return "UNKNOWN"; } diff --git a/core/res/res/drawable/ic_corp_badge.xml b/core/res/res/drawable/ic_corp_badge.xml index 16df45290302..6c40f10a03c3 100644 --- a/core/res/res/drawable/ic_corp_badge.xml +++ b/core/res/res/drawable/ic_corp_badge.xml @@ -22,8 +22,5 @@ Copyright (C) 2018 The Android Open Source Project android:viewportHeight="24"> <path android:fillColor="@android:color/white" - android:pathData="M20,6h-4V4c0-1.1-0.9-2-2-2h-4C8.9,2,8,2.9,8,4v2H4C2.9,6,2,6.9,2,8l0,11c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2V8 C22,6.9,21.1,6,20,6z M10,4h4v2h-4V4z M20,19H4V8h16V19z" /> - <path - android:fillColor="@android:color/white" - android:pathData="M 12 12 C 12.8284271247 12 13.5 12.6715728753 13.5 13.5 C 13.5 14.3284271247 12.8284271247 15 12 15 C 11.1715728753 15 10.5 14.3284271247 10.5 13.5 C 10.5 12.6715728753 11.1715728753 12 12 12 Z" /> + android:pathData="@string/config_work_badge_path_24"/> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_corp_badge_case.xml b/core/res/res/drawable/ic_corp_badge_case.xml index 1cd995eea171..838426ecd9a0 100644 --- a/core/res/res/drawable/ic_corp_badge_case.xml +++ b/core/res/res/drawable/ic_corp_badge_case.xml @@ -1,9 +1,9 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" - android:viewportWidth="20.0" - android:viewportHeight="20.0"> + android:viewportWidth="24.0" + android:viewportHeight="24.0"> <path - android:pathData="M14.67,6.5h-2.33V5.33c0,-0.65 -0.52,-1.17 -1.17,-1.17H8.83c-0.65,0 -1.17,0.52 -1.17,1.17V6.5H5.33c-0.65,0 -1.16,0.52 -1.16,1.17l-0.01,6.42c0,0.65 0.52,1.17 1.17,1.17h9.33c0.65,0 1.17,-0.52 1.17,-1.17V7.67C15.83,7.02 15.31,6.5 14.67,6.5zM10,11.75c-0.64,0 -1.17,-0.52 -1.17,-1.17c0,-0.64 0.52,-1.17 1.17,-1.17c0.64,0 1.17,0.52 1.17,1.17C11.17,11.22 10.64,11.75 10,11.75zM11.17,6.5H8.83V5.33h2.33V6.5z" + android:pathData="@string/config_work_badge_path_24" android:fillColor="#1A73E8"/> </vector> diff --git a/core/res/res/drawable/ic_corp_badge_no_background.xml b/core/res/res/drawable/ic_corp_badge_no_background.xml index 8f7fb70d5b83..e81e69f1b5d5 100644 --- a/core/res/res/drawable/ic_corp_badge_no_background.xml +++ b/core/res/res/drawable/ic_corp_badge_no_background.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> <path - android:pathData="M20,6h-4V4c0,-1.11 -0.89,-2 -2,-2h-4C8.89,2 8,2.89 8,4v2H4C2.89,6 2.01,6.89 2.01,8L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2V8C22,6.89 21.11,6 20,6zM12,15c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S13.1,15 12,15zM14,6h-4V4h4V6z" + android:pathData="@string/config_work_badge_path_24" android:fillColor="#FFFFFF"/> </vector> diff --git a/core/res/res/drawable/ic_corp_icon.xml b/core/res/res/drawable/ic_corp_icon.xml index 48531dd8c539..86bb98e0996b 100644 --- a/core/res/res/drawable/ic_corp_icon.xml +++ b/core/res/res/drawable/ic_corp_icon.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> <path - android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM12,15c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM14,6h-4L10,4h4v2z" + android:pathData="@string/config_work_badge_path_24" android:fillColor="#000000"/> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_corp_icon_badge_case.xml b/core/res/res/drawable/ic_corp_icon_badge_case.xml index 50551d401120..09cf9cb89abe 100644 --- a/core/res/res/drawable/ic_corp_icon_badge_case.xml +++ b/core/res/res/drawable/ic_corp_icon_badge_case.xml @@ -22,9 +22,15 @@ Copyright (C) 2016 The Android Open Source Project <path android:pathData="M 42 42 L 58 42 L 58 58 L 42 58 L 42 42 Z" /> - <path - android:fillColor="#1A73E8" - android:pathData="M55.33,46H52.67V44.67a1.33,1.33,0,0,0-1.33-1.33H48.67a1.33,1.33,0,0,0-1.33,1.33V46H44.67a1.32,1.32,0,0,0-1.33,1.33v7.33A1.33,1.33,0,0,0,44.67,56H55.33a1.33,1.33,0,0,0,1.33-1.33V47.33A1.33,1.33,0,0,0,55.33,46ZM50,52a1.33,1.33,0,1,1,1.33-1.33A1.34,1.34,0,0,1,50,52Zm1.33-6H48.67V44.67h2.67Z" /> + <group + android:scaleX=".66" + android:scaleY=".66" + android:translateX="42" + android:translateY="42"> + <path + android:pathData="@string/config_work_badge_path_24" + android:fillColor="#1A73E8"/> + </group> <path android:pathData="M 0 0 H 64 V 64 H 0 V 0 Z" /> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_corp_statusbar_icon.xml b/core/res/res/drawable/ic_corp_statusbar_icon.xml index 8f7fb70d5b83..e81e69f1b5d5 100644 --- a/core/res/res/drawable/ic_corp_statusbar_icon.xml +++ b/core/res/res/drawable/ic_corp_statusbar_icon.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> <path - android:pathData="M20,6h-4V4c0,-1.11 -0.89,-2 -2,-2h-4C8.89,2 8,2.89 8,4v2H4C2.89,6 2.01,6.89 2.01,8L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2V8C22,6.89 21.11,6 20,6zM12,15c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S13.1,15 12,15zM14,6h-4V4h4V6z" + android:pathData="@string/config_work_badge_path_24" android:fillColor="#FFFFFF"/> </vector> diff --git a/core/res/res/drawable/ic_corp_user_badge.xml b/core/res/res/drawable/ic_corp_user_badge.xml index a08f2d4845e1..76ba4506426d 100644 --- a/core/res/res/drawable/ic_corp_user_badge.xml +++ b/core/res/res/drawable/ic_corp_user_badge.xml @@ -8,9 +8,6 @@ android:pathData="M16.3,11.3h3.4v1.7h-3.4z" android:fillColor="#FFFFFF"/> <path - android:pathData="M18,17.17c-0.92,0 -1.67,0.75 -1.67,1.67c0,0.92 0.75,1.67 1.67,1.67c0.92,0 1.67,-0.75 1.67,-1.67C19.67,17.92 18.92,17.17 18,17.17z" - android:fillColor="#FFFFFF"/> - <path android:pathData="M18,0C8.06,0 0,8.06 0,18s8.06,18 18,18s18,-8.06 18,-18S27.94,0 18,0zM26.3,23.83c0,0.92 -0.71,1.67 -1.63,1.67H11.33c-0.93,0 -1.67,-0.74 -1.67,-1.67l0.01,-9.17c0,-0.92 0.73,-1.67 1.66,-1.67h3.37v-1.67c0,-0.93 0.71,-1.63 1.63,-1.63h3.33c0.93,0 1.63,0.71 1.63,1.63V13h3.37c0.93,0 1.63,0.74 1.63,1.67V23.83z" android:fillColor="#FFFFFF"/> </vector> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index cbe0eef6408b..219d0650588f 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4533,6 +4533,13 @@ If non-positive, then the refresh rate is unchanged even if thresholds are configured. --> <integer name="config_fixedRefreshRateInHighZone">0</integer> + <!-- Default refresh rate while the device has high brightness mode enabled for Sunlight. + This value overrides values from DisplayDeviceConfig --> + <integer name="config_defaultRefreshRateInHbmSunlight">0</integer> + + <!-- Default refresh rate while the device has high brightness mode enabled for HDR. --> + <integer name="config_defaultRefreshRateInHbmHdr">0</integer> + <!-- The type of the light sensor to be used by the display framework for things like auto-brightness. If unset, then it just gets the default sensor of type TYPE_LIGHT. --> <string name="config_displayLightSensorType" translatable="false" /> @@ -5260,4 +5267,9 @@ <item>@dimen/rounded_corner_radius_bottom_adjustment</item> <item>@dimen/secondary_rounded_corner_radius_bottom_adjustment</item> </array> + + <!-- Shape of the work badge icon for viewport size 24. --> + <string name="config_work_badge_path_24" translatable="false"> + M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2z + </string> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index fbc629e2dc81..4e8d915ca295 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3964,6 +3964,8 @@ <java-symbol type="integer" name="config_defaultRefreshRateInZone" /> <java-symbol type="array" name="config_brightnessThresholdsOfPeakRefreshRate" /> <java-symbol type="array" name="config_ambientThresholdsOfPeakRefreshRate" /> + <java-symbol type="integer" name="config_defaultRefreshRateInHbmSunlight" /> + <java-symbol type="integer" name="config_defaultRefreshRateInHbmHdr" /> <!-- For fixed refresh rate displays in high brightness--> <java-symbol type="integer" name="config_fixedRefreshRateInHighZone" /> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 791aeb73ffa3..909ca3988c5d 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -3505,6 +3505,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1805116444": { + "message": "We don't support remote animation for Task with multiple TaskFragmentOrganizers.", + "level": "ERROR", + "group": "WM_DEBUG_APP_TRANSITIONS", + "at": "com\/android\/server\/wm\/AppTransitionController.java" + }, "1810019902": { "message": "TRANSIT_FLAG_OPEN_BEHIND, adding %s to mOpeningApps", "level": "DEBUG", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index 3ab062499845..38079aff9a6f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -537,6 +537,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( mContext.getContentResolver(), mUserId); setSwipeToNotificationEnabled(enabled); + notifyShortcutStateChanged(mState.getState()); mOneHandedUiEventLogger.writeEvent(enabled ? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_SHOW_NOTIFICATION_ENABLED_ON 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 493870d7fd72..a046c42b2391 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 @@ -250,7 +250,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, setSideStagePosition(sideStagePosition, wct); mMainStage.activate(getMainStageBounds(), wct); mSideStage.addTask(task, getSideStageBounds(), wct); - mTaskOrganizer.applyTransaction(wct); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> updateSurfaceBounds(null /* layout */, t)); return true; } @@ -446,15 +447,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, setSideStagePosition(sideStagePosition, true /* updateBounds */, wct); } - private void setSideStagePosition(@SplitPosition int sideStagePosition, - boolean updateBounds, @Nullable WindowContainerTransaction wct) { + private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds, + @Nullable WindowContainerTransaction wct) { if (mSideStagePosition == sideStagePosition) return; mSideStagePosition = sideStagePosition; sendOnStagePositionChanged(); if (mSideStageListener.mVisible && updateBounds) { if (wct == null) { - // onBoundsChanged builds/applies a wct with the contents of updateWindowBounds. + // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds. onLayoutChanged(mSplitLayout); } else { updateWindowBounds(mSplitLayout, wct); @@ -675,29 +676,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } mSyncQueue.runInSync(t -> { - final SurfaceControl sideStageLeash = mSideStage.mRootLeash; - final SurfaceControl mainStageLeash = mMainStage.mRootLeash; - - if (sideStageVisible) { - final Rect sideStageBounds = getSideStageBounds(); - t.setPosition(sideStageLeash, - sideStageBounds.left, sideStageBounds.top) - .setWindowCrop(sideStageLeash, - sideStageBounds.width(), sideStageBounds.height()); - } - - if (mainStageVisible) { - final Rect mainStageBounds = getMainStageBounds(); - t.setPosition(mainStageLeash, mainStageBounds.left, mainStageBounds.top) - .setWindowCrop(mainStageLeash, - mainStageBounds.width(), mainStageBounds.height()); - } - // Same above, we only set root tasks and divider leash visibility when both stage // change to visible or invisible to avoid flicker. if (sameVisibility) { - t.setVisibility(sideStageLeash, bothStageVisible) - .setVisibility(mainStageLeash, bothStageVisible); + t.setVisibility(mSideStage.mRootLeash, bothStageVisible) + .setVisibility(mMainStage.mRootLeash, bothStageVisible); applyDividerVisibility(t); } }); diff --git a/location/java/android/location/GpsNavigationMessage.java b/location/java/android/location/GpsNavigationMessage.java index dc1e99fd6a4f..2b978f759a13 100644 --- a/location/java/android/location/GpsNavigationMessage.java +++ b/location/java/android/location/GpsNavigationMessage.java @@ -262,12 +262,8 @@ public class GpsNavigationMessage implements Parcelable { parcel.readByteArray(data); navigationMessage.setData(data); - if (parcel.dataAvail() >= Integer.SIZE) { - int status = parcel.readInt(); - navigationMessage.setStatus((short) status); - } else { - navigationMessage.setStatus(STATUS_UNKNOWN); - } + int status = parcel.readInt(); + navigationMessage.setStatus((short) status); return navigationMessage; } diff --git a/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml b/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml new file mode 100644 index 000000000000..c6b87d38f7da --- /dev/null +++ b/packages/SystemUI/animation/res/anim/launch_host_dialog_enter.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- The enter animation of the host dialog is a translation of 0px that lasts 500ms so that the --> +<!-- host dialog is directly visible but the dim background still takes 500ms to fade in. --> +<translate xmlns:android="http://schemas.android.com/apk/res/android" + android:fromXDelta="0" + android:toXDelta="0" + android:duration="500" />
\ No newline at end of file diff --git a/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml b/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml new file mode 100644 index 000000000000..a0f441eaeed4 --- /dev/null +++ b/packages/SystemUI/animation/res/anim/launch_host_dialog_exit.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<alpha + xmlns:android="http://schemas.android.com/apk/res/android" + android:interpolator="@android:interpolator/decelerate_cubic" + android:duration="150" + android:fromAlpha="1.0" + android:toAlpha="0.0" />
\ No newline at end of file diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml new file mode 100644 index 000000000000..ef60a248f79a --- /dev/null +++ b/packages/SystemUI/animation/res/values/ids.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <item type="id" name="launch_animation_running"/> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/animation/res/values/styles.xml b/packages/SystemUI/animation/res/values/styles.xml new file mode 100644 index 000000000000..ad06c9192bc3 --- /dev/null +++ b/packages/SystemUI/animation/res/values/styles.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <style name="HostDialogTheme"> + <item name="android:windowAnimationStyle">@style/Animation.HostDialog</item> + <item name="android:windowIsFloating">false</item> + <item name="android:backgroundDimEnabled">true</item> + <item name="android:navigationBarColor">@android:color/transparent</item> + </style> + + <style name="Animation.HostDialog" parent="@android:style/Animation"> + <item name="android:windowEnterAnimation">@anim/launch_host_dialog_enter</item> + <item name="android:windowExitAnimation">@anim/launch_host_dialog_exit</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 9c1e12923b43..702060338359 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -1,23 +1,31 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.android.systemui.animation -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.ValueAnimator import android.app.ActivityManager import android.app.ActivityTaskManager import android.app.PendingIntent import android.app.TaskInfo -import android.content.Context import android.graphics.Matrix -import android.graphics.PorterDuff -import android.graphics.PorterDuffXfermode import android.graphics.Rect import android.graphics.RectF -import android.graphics.drawable.GradientDrawable import android.os.Looper import android.os.RemoteException import android.util.Log -import android.util.MathUtils import android.view.IRemoteAnimationFinishedCallback import android.view.IRemoteAnimationRunner import android.view.RemoteAnimationAdapter @@ -26,7 +34,6 @@ import android.view.SyncRtSurfaceTransactionApplier import android.view.View import android.view.ViewGroup import android.view.WindowManager -import android.view.animation.AnimationUtils import android.view.animation.PathInterpolator import com.android.internal.annotations.VisibleForTesting import com.android.internal.policy.ScreenDecorationsUtils @@ -38,52 +45,23 @@ private const val TAG = "ActivityLaunchAnimator" * A class that allows activities to be started in a seamless way from a view that is transforming * nicely into the starting window. */ -class ActivityLaunchAnimator( - private val callback: Callback, - context: Context -) { +class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) { companion object { - private const val DEBUG = false - const val ANIMATION_DURATION = 500L - private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L - private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L - private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT private const val ANIMATION_DURATION_NAV_FADE_IN = 266L private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L private const val ANIMATION_DELAY_NAV_FADE_IN = - ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN + LaunchAnimator.ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN private const val LAUNCH_TIMEOUT = 1000L - @JvmField val CONTENT_FADE_OUT_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f) - private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f) private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f) private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f) - - private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC) - - /** - * Given the [linearProgress] of a launch animation, return the linear progress of the - * sub-animation starting [delay] ms after the launch animation and that lasts [duration]. - */ - @JvmStatic - fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float { - return MathUtils.constrain( - (linearProgress * ANIMATION_DURATION - delay) / duration, - 0.0f, - 1.0f - ) - } } - /** The interpolator used for the width, height, Y position and corner radius. */ - private val animationInterpolator = AnimationUtils.loadInterpolator(context, - R.interpolator.launch_animation_interpolator_y) - - /** The interpolator used for the X position. */ - private val animationInterpolatorX = AnimationUtils.loadInterpolator(context, - R.interpolator.launch_animation_interpolator_x) - - private val cornerRadii = FloatArray(8) + /** + * The callback of this animator. This should be set before any call to + * [start(Pending)IntentWithAnimation]. + */ + var callback: Callback? = null /** * Start an intent and animate the opening window. The intent will be started by running @@ -119,6 +97,8 @@ class ActivityLaunchAnimator( return } + val callback = this.callback ?: throw IllegalStateException( + "ActivityLaunchAnimator.callback must be set before using this animator") val runner = Runner(controller) val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen @@ -126,9 +106,9 @@ class ActivityLaunchAnimator( // keyguard with the animation val animationAdapter = if (!hideKeyguardWithAnimation) { RemoteAnimationAdapter( - runner, - ANIMATION_DURATION, - ANIMATION_DURATION - 150 /* statusBarTransitionDelay */ + runner, + LaunchAnimator.ANIMATION_DURATION, + LaunchAnimator.ANIMATION_DURATION - 150 /* statusBarTransitionDelay */ ) } else { null @@ -150,10 +130,10 @@ class ActivityLaunchAnimator( // Only animate if the app is not already on top and will be opened, unless we are on the // keyguard. val willAnimate = - launchResult == ActivityManager.START_TASK_TO_FRONT || - launchResult == ActivityManager.START_SUCCESS || - (launchResult == ActivityManager.START_DELIVERED_TO_TOP && - hideKeyguardWithAnimation) + launchResult == ActivityManager.START_TASK_TO_FRONT || + launchResult == ActivityManager.START_SUCCESS || + (launchResult == ActivityManager.START_DELIVERED_TO_TOP && + hideKeyguardWithAnimation) Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " + "hideKeyguardWithAnimation=$hideKeyguardWithAnimation") @@ -234,7 +214,7 @@ class ActivityLaunchAnimator( * * Note that all callbacks (onXXX methods) are all called on the main thread. */ - interface Controller { + interface Controller : LaunchAnimator.Controller { companion object { /** * Return a [Controller] that will animate and expand [view] into the opening window. @@ -259,53 +239,12 @@ class ActivityLaunchAnimator( } /** - * The container in which the view that started the intent will be animating together with - * the opening window. - * - * This will be used to: - * - Get the associated [Context]. - * - Compute whether we are expanding fully above the current window. - * - Apply surface transactions in sync with RenderThread. - * - * This container can be changed to force this [Controller] to animate the expanding view - * inside a different location, for instance to ensure correct layering during the - * animation. - */ - var launchContainer: ViewGroup - - /** - * Return the [State] of the view that will be animated. We will animate from this state to - * the final window state. - * - * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the - * animation. - */ - fun createAnimatorState(): State - - /** * The intent was started. If [willAnimate] is false, nothing else will happen and the * animation will not be started. */ fun onIntentStarted(willAnimate: Boolean) {} /** - * The animation started. This is typically used to initialize any additional resource - * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding - * fully above the [root view][getRootView]. - */ - fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {} - - /** The animation made progress and the expandable view [state] should be updated. */ - fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {} - - /** - * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was - * called previously. This is typically used to clean up the resources initialized when the - * animation was started. - */ - fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {} - - /** * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called * before the cancellation. @@ -313,66 +252,11 @@ class ActivityLaunchAnimator( fun onLaunchAnimationCancelled() {} } - /** The state of an expandable view during an [ActivityLaunchAnimator] animation. */ - open class State( - /** The position of the view in screen space coordinates. */ - var top: Int, - var bottom: Int, - var left: Int, - var right: Int, - - var topCornerRadius: Float = 0f, - var bottomCornerRadius: Float = 0f - ) { - private val startTop = top - private val startBottom = bottom - private val startLeft = left - private val startRight = right - private val startWidth = width - private val startHeight = height - val startCenterX = centerX - val startCenterY = centerY - - val width: Int - get() = right - left - - val height: Int - get() = bottom - top - - open val topChange: Int - get() = top - startTop - - open val bottomChange: Int - get() = bottom - startBottom - - val leftChange: Int - get() = left - startLeft - - val rightChange: Int - get() = right - startRight - - val widthRatio: Float - get() = width.toFloat() / startWidth - - val heightRatio: Float - get() = height.toFloat() / startHeight - - val centerX: Float - get() = left + width / 2f - - val centerY: Float - get() = top + height / 2f - - /** Whether the expanded view should be visible or hidden. */ - var visible: Boolean = true - } - @VisibleForTesting inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() { private val launchContainer = controller.launchContainer private val context = launchContainer.context private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer) - private var animator: ValueAnimator? = null private val matrix = Matrix() private val invertMatrix = Matrix() @@ -380,6 +264,7 @@ class ActivityLaunchAnimator( private var windowCropF = RectF() private var timedOut = false private var cancelled = false + private var animation: LaunchAnimator.Animation? = null // A timeout to cancel the remote animation if it is not started within X milliseconds after // the intent was started. @@ -429,7 +314,7 @@ class ActivityLaunchAnimator( nonApps: Array<out RemoteAnimationTarget>?, iCallback: IRemoteAnimationFinishedCallback? ) { - if (DEBUG) { + if (LaunchAnimator.DEBUG) { Log.d(TAG, "Remote animation started") } @@ -449,36 +334,20 @@ class ActivityLaunchAnimator( it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR } - // Start state. - val state = controller.createAnimatorState() - - val startTop = state.top - val startBottom = state.bottom - val startLeft = state.left - val startRight = state.right - val startXCenter = (startLeft + startRight) / 2f - val startWidth = startRight - startLeft - - val startTopCornerRadius = state.topCornerRadius - val startBottomCornerRadius = state.bottomCornerRadius - - // End state. val windowBounds = window.screenSpaceBounds - val endTop = windowBounds.top - val endBottom = windowBounds.bottom - val endLeft = windowBounds.left - val endRight = windowBounds.right - val endXCenter = (endLeft + endRight) / 2f - val endWidth = endRight - endLeft - - // TODO(b/184121838): Ensure that we are launching on the same screen. - val rootViewLocation = launchContainer.locationOnScreen - val isExpandingFullyAbove = endTop <= rootViewLocation[1] && - endBottom >= rootViewLocation[1] + launchContainer.height && - endLeft <= rootViewLocation[0] && - endRight >= rootViewLocation[0] + launchContainer.width - - // TODO(b/184121838): We should somehow get the top and bottom radius of the window. + val endState = LaunchAnimator.State( + top = windowBounds.top, + bottom = windowBounds.bottom, + left = windowBounds.left, + right = windowBounds.right + ) + val callback = this@ActivityLaunchAnimator.callback!! + val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo) + + // TODO(b/184121838): We should somehow get the top and bottom radius of the window + // instead of recomputing isExpandingFullyAbove here. + val isExpandingFullyAbove = + launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState) val endRadius = if (isExpandingFullyAbove) { // Most of the time, expanding fully above the root view means expanding in full // screen. @@ -488,97 +357,40 @@ class ActivityLaunchAnimator( // a radius of 0. 0f } + endState.topCornerRadius = endRadius + endState.bottomCornerRadius = endRadius - // We add an extra layer with the same color as the app splash screen background color, - // which is usually the same color of the app background. We first fade in this layer - // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the - // launch container and reveal the opening window. - val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo) - val windowBackgroundLayer = GradientDrawable().apply { - setColor(windowBackgroundColor) - alpha = 0 - } - - // Update state. - val animator = ValueAnimator.ofFloat(0f, 1f) - this.animator = animator - animator.duration = ANIMATION_DURATION - animator.interpolator = Interpolators.LINEAR - - val launchContainerOverlay = launchContainer.overlay - animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { - if (DEBUG) { - Log.d(TAG, "Animation started") - } - + // We animate the opening window and delegate the view expansion to [this.controller]. + val delegate = this.controller + val controller = object : LaunchAnimator.Controller by delegate { + override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { callback.setBlursDisabledForAppLaunch(true) - controller.onLaunchAnimationStart(isExpandingFullyAbove) - - // Add the drawable to the launch container overlay. Overlays always draw - // drawables after views, so we know that it will be drawn above any view added - // by the controller. - launchContainerOverlay.add(windowBackgroundLayer) + delegate.onLaunchAnimationStart(isExpandingFullyAbove) } - override fun onAnimationEnd(animation: Animator?) { - if (DEBUG) { - Log.d(TAG, "Animation ended") - } - + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { callback.setBlursDisabledForAppLaunch(false) iCallback?.invoke() - controller.onLaunchAnimationEnd(isExpandingFullyAbove) - launchContainerOverlay.remove(windowBackgroundLayer) + delegate.onLaunchAnimationEnd(isExpandingFullyAbove) } - }) - animator.addUpdateListener { animation -> - if (cancelled) { - return@addUpdateListener + override fun onLaunchAnimationProgress( + state: LaunchAnimator.State, + progress: Float, + linearProgress: Float + ) { + applyStateToWindow(window, state) + navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } + delegate.onLaunchAnimationProgress(state, progress, linearProgress) } - - val linearProgress = animation.animatedFraction - val progress = animationInterpolator.getInterpolation(linearProgress) - val xProgress = animationInterpolatorX.getInterpolation(linearProgress) - val xCenter = MathUtils.lerp(startXCenter, endXCenter, xProgress) - val halfWidth = lerp(startWidth, endWidth, progress) / 2 - - state.top = lerp(startTop, endTop, progress).roundToInt() - state.bottom = lerp(startBottom, endBottom, progress).roundToInt() - state.left = (xCenter - halfWidth).roundToInt() - state.right = (xCenter + halfWidth).roundToInt() - - state.topCornerRadius = MathUtils.lerp(startTopCornerRadius, endRadius, progress) - state.bottomCornerRadius = - MathUtils.lerp(startBottomCornerRadius, endRadius, progress) - - // The expanding view can/should be hidden once it is completely coverred by the - // windowBackgroundLayer. - state.visible = - getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1 - - applyStateToWindow(window, state) - applyStateToWindowBackgroundLayer(windowBackgroundLayer, state, linearProgress) - navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } - - // If we started expanding the view, we make it 1 pixel smaller on all sides to - // avoid artefacts on the corners caused by anti-aliasing of the view background and - // the window background layer. - if (state.top != startTop && state.left != startLeft && - state.bottom != startBottom && state.right != startRight) { - state.top += 1 - state.left += 1 - state.right -= 1 - state.bottom -= 1 - } - controller.onLaunchAnimationProgress(state, progress, linearProgress) } - animator.start() + // We draw a hole when the additional layer is fading out to reveal the opening window. + animation = launchAnimator.startAnimation( + controller, endState, windowBackgroundColor, drawHole = true) } - private fun applyStateToWindow(window: RemoteAnimationTarget, state: State) { + private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) { val screenBounds = window.screenSpaceBounds val centerX = (screenBounds.left + screenBounds.right) / 2f val centerY = (screenBounds.top + screenBounds.bottom) / 2f @@ -632,48 +444,13 @@ class ActivityLaunchAnimator( transactionApplier.scheduleApply(params) } - private fun applyStateToWindowBackgroundLayer( - drawable: GradientDrawable, - state: State, - linearProgress: Float - ) { - // Update position. - drawable.setBounds(state.left, state.top, state.right, state.bottom) - - // Update radius. - cornerRadii[0] = state.topCornerRadius - cornerRadii[1] = state.topCornerRadius - cornerRadii[2] = state.topCornerRadius - cornerRadii[3] = state.topCornerRadius - cornerRadii[4] = state.bottomCornerRadius - cornerRadii[5] = state.bottomCornerRadius - cornerRadii[6] = state.bottomCornerRadius - cornerRadii[7] = state.bottomCornerRadius - drawable.cornerRadii = cornerRadii - - // We first fade in the background layer to hide the expanding view, then fade it out - // with SRC mode to draw a hole punch in the status bar and reveal the opening window. - val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) - if (fadeInProgress < 1) { - val alpha = CONTENT_FADE_OUT_INTERPOLATOR.getInterpolation(fadeInProgress) - drawable.alpha = (alpha * 0xFF).roundToInt() - drawable.setXfermode(null) - } else { - val fadeOutProgress = getProgress(linearProgress, - ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW) - val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress) - drawable.alpha = (alpha * 0xFF).roundToInt() - drawable.setXfermode(SRC_MODE) - } - } - private fun applyStateToNavigationBar( navigationBar: RemoteAnimationTarget, - state: State, + state: LaunchAnimator.State, linearProgress: Float ) { - val fadeInProgress = getProgress(linearProgress, ANIMATION_DELAY_NAV_FADE_IN, - ANIMATION_DURATION_NAV_FADE_OUT) + val fadeInProgress = LaunchAnimator.getProgress(linearProgress, + ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT) val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash) if (fadeInProgress > 0) { @@ -682,13 +459,13 @@ class ActivityLaunchAnimator( 0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat()) windowCrop.set(state.left, 0, state.right, state.height) params - .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress)) - .withMatrix(matrix) - .withWindowCrop(windowCrop) - .withVisibility(true) + .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress)) + .withMatrix(matrix) + .withWindowCrop(windowCrop) + .withVisibility(true) } else { - val fadeOutProgress = getProgress(linearProgress, 0, - ANIMATION_DURATION_NAV_FADE_OUT) + val fadeOutProgress = LaunchAnimator.getProgress(linearProgress, 0, + ANIMATION_DURATION_NAV_FADE_OUT) params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress)) } @@ -714,7 +491,7 @@ class ActivityLaunchAnimator( cancelled = true removeTimeout() context.mainExecutor.execute { - animator?.cancel() + animation?.cancel() controller.onLaunchAnimationCancelled() } } @@ -726,9 +503,5 @@ class ActivityLaunchAnimator( e.printStackTrace() } } - - private fun lerp(start: Int, stop: Int, amount: Float): Float { - return MathUtils.lerp(start.toFloat(), stop.toFloat(), amount) - } } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt index d4be25382395..258ca6bdf79b 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DelegateLaunchAnimatorController.kt @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.android.systemui.animation /** diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt new file mode 100644 index 000000000000..c2b36089d0a7 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -0,0 +1,541 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation + +import android.app.Dialog +import android.content.Context +import android.graphics.Color +import android.os.Looper +import android.util.Log +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.view.WindowManager +import android.widget.FrameLayout + +private const val TAG = "DialogLaunchAnimator" + +/** + * A class that allows dialogs to be started in a seamless way from a view that is transforming + * nicely into the starting dialog. + * + * Important: Don't forget to call [DialogLaunchAnimator.onDozeAmountChanged] when the doze amount + * changes to gracefully handle dialogs fading out when the device is dozing. + */ +class DialogLaunchAnimator( + private val context: Context, + private val launchAnimator: LaunchAnimator, + private val hostDialogProvider: HostDialogProvider +) { + private companion object { + private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running + } + + // TODO(b/201264644): Remove this set. + private val currentAnimations = hashSetOf<DialogLaunchAnimation>() + + /** + * Show [dialog] by expanding it from [view]. + * + * Caveats: When calling this function, the dialog content view will actually be stolen and + * attached to a different dialog (and thus a different window) which means that the actual + * dialog window will never be drawn. Moreover, unless [dialog] is a [ListenableDialog], you + * must call dismiss(), hide() and show() on the [Dialog] returned by this function to actually + * dismiss, hide or show the dialog. + */ + fun showFromView(dialog: Dialog, view: View): Dialog { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw IllegalStateException( + "showFromView must be called from the main thread and dialog must be created in " + + "the main thread") + } + + // Make sure we don't run the launch animation from the same view twice at the same time. + if (view.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) { + Log.e(TAG, "Not running dialog launch animation as there is already one running") + dialog.show() + return dialog + } + + view.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true) + + val launchAnimation = DialogLaunchAnimation( + context, launchAnimator, hostDialogProvider, view, + onDialogDismissed = { currentAnimations.remove(it) }, originalDialog = dialog) + val hostDialog = launchAnimation.hostDialog + currentAnimations.add(launchAnimation) + + // If the dialog is dismissed/hidden/shown, then we should actually dismiss/hide/show the + // host dialog. + if (dialog is ListenableDialog) { + dialog.addListener(object : DialogListener { + override fun onDismiss() { + dialog.removeListener(this) + hostDialog.dismiss() + } + + override fun onHide() { + if (launchAnimation.ignoreNextCallToHide) { + launchAnimation.ignoreNextCallToHide = false + return + } + + hostDialog.hide() + } + + override fun onShow() { + hostDialog.show() + + // We don't actually want to show the original dialog, so hide it. + launchAnimation.ignoreNextCallToHide = true + dialog.hide() + } + }) + } + + launchAnimation.start() + return hostDialog + } + + /** Notify the current doze amount, to ensure that dialogs fade out when dozing. */ + // TODO(b/193634619): Replace this by some mandatory constructor parameter to make sure that we + // don't forget to call this when the doze amount changes. + fun onDozeAmountChanged(amount: Float) { + currentAnimations.forEach { it.onDozeAmountChanged(amount) } + } + + /** + * Ensure that all dialogs currently shown won't animate into their touch surface when + * dismissed. + * + * This is a temporary API meant to be called right before we both dismiss a dialog and start + * an activity, which currently does not look good if we animate the dialog into the touch + * surface at the same time as the activity starts. + * + * TODO(b/193634619): Remove this function and animate dialog into opening activity instead. + */ + fun disableAllCurrentDialogsExitAnimations() { + currentAnimations.forEach { it.exitAnimationDisabled = true } + } +} + +interface HostDialogProvider { + /** + * Create a host dialog that will be used to host a launch animation. This host dialog must: + * 1. call [onCreateCallback] in its onCreate() method, e.g. right after calling + * super.onCreate(). + * 2. call [dismissOverride] instead of doing any dismissing logic. The actual dismissing + * logic should instead be done inside the lambda passed to [dismissOverride], which will + * be called after the exit animation. + * + * See SystemUIHostDialogProvider for an example of implementation. + */ + fun createHostDialog( + context: Context, + theme: Int, + onCreateCallback: () -> Unit, + dismissOverride: (() -> Unit) -> Unit + ): Dialog +} + +/** A dialog to/from which we can add/remove listeners. */ +interface ListenableDialog { + /** Add [listener] to the listeners. */ + fun addListener(listener: DialogListener) + + /** Remove [listener] from the listeners. */ + fun removeListener(listener: DialogListener) +} + +interface DialogListener { + /** Called when this dialog dismiss() is called. */ + fun onDismiss() + + /** Called when this dialog hide() is called. */ + fun onHide() + + /** Called when this dialog show() is called. */ + fun onShow() +} + +private class DialogLaunchAnimation( + private val context: Context, + private val launchAnimator: LaunchAnimator, + hostDialogProvider: HostDialogProvider, + + /** The view that triggered the dialog after being tapped. */ + private val touchSurface: View, + + /** + * A callback that will be called with this [DialogLaunchAnimation] after the dialog was + * dismissed and the exit animation is done. + */ + private val onDialogDismissed: (DialogLaunchAnimation) -> Unit, + + /** The original dialog whose content will be shown and animate in/out in [hostDialog]. */ + private val originalDialog: Dialog +) { + /** + * The fullscreen dialog to which we will add the content view [originalDialogView] of + * [originalDialog]. + */ + val hostDialog = hostDialogProvider.createHostDialog( + context, R.style.HostDialogTheme, this::onHostDialogCreated, this::onHostDialogDismissed) + + /** The root content view of [hostDialog]. */ + private val hostDialogRoot = FrameLayout(context) + + /** + * The content view of [originalDialog], which will be stolen from that dialog and added to + * [hostDialogRoot]. + */ + private var originalDialogView: View? = null + + /** + * The background color of [originalDialogView], taking into consideration the [originalDialog] + * window background color. + */ + private var originalDialogBackgroundColor = Color.BLACK + + /** + * Whether we are currently launching/showing the dialog by animating it from [touchSurface]. + */ + private var isLaunching = true + + /** Whether we are currently dismissing/hiding the dialog by animating into [touchSurface]. */ + private var isDismissing = false + + private var dismissRequested = false + private var drawHostDialog = false + var ignoreNextCallToHide = false + var exitAnimationDisabled = false + + fun start() { + // Show the host (fullscreen) dialog, to which we will add the stolen dialog view. + hostDialog.show() + + // Steal the dialog view. We do that by showing it but preventing it from drawing, then + // hiding it as soon as its content is available. + stealOriginalDialogContentView(then = this::showDialogFromView) + } + + private fun onHostDialogCreated() { + // Make the dialog fullscreen with a transparent background. + hostDialog.setContentView( + hostDialogRoot, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + + val window = hostDialog.window + ?: throw IllegalStateException("There is no window associated to the host dialog") + window.setBackgroundDrawableResource(android.R.color.transparent) + window.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT + ) + + // Prevent the host dialog from drawing until the animation starts. + hostDialogRoot.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + if (drawHostDialog) { + hostDialogRoot.viewTreeObserver.removeOnPreDrawListener(this) + return true + } + + return false + } + } + ) + } + + /** Get the content view of [originalDialog] and pass it to [then]. */ + private fun stealOriginalDialogContentView(then: (View) -> Unit) { + // The original dialog content view will be attached to android.R.id.content when the dialog + // is shown, so we show the dialog and add an observer to get the view but also prevents the + // original dialog from being drawn. + val androidContent = originalDialog.findViewById<ViewGroup>(android.R.id.content) + ?: throw IllegalStateException("Dialog does not have any android.R.id.content view") + + androidContent.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + if (androidContent.childCount == 1) { + androidContent.viewTreeObserver.removeOnPreDrawListener(this) + + // Hide the animated dialog. Because of the dialog listener set up + // earlier, this would also hide the host dialog, but in this case we + // need to keep the host dialog visible. + ignoreNextCallToHide = true + originalDialog.hide() + + then(androidContent.getChildAt(0)) + return false + } + + // Never draw the original dialog content. + return false + } + }) + originalDialog.show() + } + + private fun showDialogFromView(dialogView: View) { + // Save the dialog view for later as we will need it for the close animation. + this.originalDialogView = dialogView + + // Close the dialog when clicking outside of it. + hostDialogRoot.setOnClickListener { hostDialog.dismiss() } + dialogView.isClickable = true + + // Set the background of the window dialog to the dialog itself. + // TODO(b/193634619): Support dialog windows without background. + // TODO(b/193634619): Support dialog whose background comes from the content view instead of + // the window. + val typedArray = + originalDialog.context.obtainStyledAttributes(com.android.internal.R.styleable.Window) + val backgroundRes = + typedArray.getResourceId(com.android.internal.R.styleable.Window_windowBackground, 0) + typedArray.recycle() + if (backgroundRes == 0) { + throw IllegalStateException("Dialogs with no backgrounds on window are not supported") + } + + dialogView.setBackgroundResource(backgroundRes) + originalDialogBackgroundColor = + GhostedViewLaunchAnimatorController.findGradientDrawable(dialogView.background!!) + ?.color + ?.defaultColor ?: Color.BLACK + + // Add the dialog view to the host (fullscreen) dialog and make it invisible to make sure + // it's not drawn yet. + (dialogView.parent as? ViewGroup)?.removeView(dialogView) + hostDialogRoot.addView( + dialogView, + + // We give it the size of its original dialog window. + FrameLayout.LayoutParams( + originalDialog.window.attributes.width, + originalDialog.window.attributes.height, + Gravity.CENTER + ) + ) + dialogView.visibility = View.INVISIBLE + + // Start the animation when the dialog is laid out in the center of the host dialog. + dialogView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener { + override fun onLayoutChange( + view: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + dialogView.removeOnLayoutChangeListener(this) + startAnimation( + isLaunching = true, + onLaunchAnimationStart = { drawHostDialog = true }, + onLaunchAnimationEnd = { + touchSurface.setTag(R.id.launch_animation_running, null) + + // We hide the touch surface when the dialog is showing. We will make this + // view visible again when dismissing the dialog. + // TODO(b/193634619): Provide an easy way for views to check if they should + // be hidden because of a dialog launch so that they don't override this + // visibility when updating/refreshing itself. + touchSurface.visibility = View.INVISIBLE + + isLaunching = false + + // dismiss was called during the animation, dismiss again now to actually + // dismiss. + if (dismissRequested) { + hostDialog.dismiss() + } + } + ) + } + }) + } + + private fun onHostDialogDismissed(actualDismiss: () -> Unit) { + if (Looper.myLooper() != Looper.getMainLooper()) { + context.mainExecutor.execute { onHostDialogDismissed(actualDismiss) } + return + } + + // TODO(b/193634619): Support interrupting the launch animation in the middle. + if (isLaunching) { + dismissRequested = true + return + } + + if (isDismissing) { + return + } + + isDismissing = true + hideDialogIntoView { instantDismiss: Boolean -> + if (instantDismiss) { + originalDialog.hide() + hostDialog.hide() + } + + originalDialog.dismiss() + actualDismiss() + } + } + + /** + * Hide the dialog into the touch surface and call [dismissDialogs] when the animation is done + * (passing instantDismiss=true) or if it's skipped (passing instantDismiss=false) to actually + * dismiss the dialogs. + */ + private fun hideDialogIntoView(dismissDialogs: (Boolean) -> Unit) { + if (!shouldAnimateDialogIntoView()) { + Log.i(TAG, "Skipping animation of dialog into the touch surface") + + // If the view is invisible it's probably because of us, so we make it visible again. + if (touchSurface.visibility == View.INVISIBLE) { + touchSurface.visibility = View.VISIBLE + } + + dismissDialogs(false /* instantDismiss */) + onDialogDismissed(this@DialogLaunchAnimation) + return + } + + startAnimation( + isLaunching = false, + onLaunchAnimationStart = { + // Remove the dim background as soon as we start the animation. + hostDialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + }, + onLaunchAnimationEnd = { + touchSurface.visibility = View.VISIBLE + originalDialogView!!.visibility = View.INVISIBLE + dismissDialogs(true /* instantDismiss */) + onDialogDismissed(this@DialogLaunchAnimation) + } + ) + } + + private fun startAnimation( + isLaunching: Boolean, + onLaunchAnimationStart: () -> Unit = {}, + onLaunchAnimationEnd: () -> Unit = {} + ) { + val dialogView = this.originalDialogView!! + + // Create 2 ghost controllers to animate both the dialog and the touch surface in the host + // dialog. + val startView = if (isLaunching) touchSurface else dialogView + val endView = if (isLaunching) dialogView else touchSurface + val startViewController = GhostedViewLaunchAnimatorController(startView) + val endViewController = GhostedViewLaunchAnimatorController(endView) + startViewController.launchContainer = hostDialogRoot + endViewController.launchContainer = hostDialogRoot + + val endState = endViewController.createAnimatorState() + val controller = object : LaunchAnimator.Controller { + override var launchContainer: ViewGroup + get() = startViewController.launchContainer + set(value) { + startViewController.launchContainer = value + endViewController.launchContainer = value + } + + override fun createAnimatorState(): LaunchAnimator.State { + return startViewController.createAnimatorState() + } + + override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + startViewController.onLaunchAnimationStart(isExpandingFullyAbove) + endViewController.onLaunchAnimationStart(isExpandingFullyAbove) + + onLaunchAnimationStart() + } + + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + startViewController.onLaunchAnimationEnd(isExpandingFullyAbove) + endViewController.onLaunchAnimationEnd(isExpandingFullyAbove) + + onLaunchAnimationEnd() + } + + override fun onLaunchAnimationProgress( + state: LaunchAnimator.State, + progress: Float, + linearProgress: Float + ) { + startViewController.onLaunchAnimationProgress(state, progress, linearProgress) + + // The end view is visible only iff the starting view is not visible. + state.visible = !state.visible + endViewController.onLaunchAnimationProgress(state, progress, linearProgress) + + // If the dialog content is complex, its dimension might change during the launch + // animation. The animation end position might also change during the exit + // animation, for instance when locking the phone when the dialog is open. Therefore + // we update the end state to the new position/size. Usually the dialog dimension or + // position will change in the early frames, so changing the end state shouldn't + // really be noticeable. + endViewController.fillGhostedViewState(endState) + } + } + + launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor) + } + + private fun shouldAnimateDialogIntoView(): Boolean { + if (exitAnimationDisabled) { + return false + } + + // The touch surface should be invisible by now, if it's not then something else changed its + // visibility and we probably don't want to run the animation. + if (touchSurface.visibility != View.INVISIBLE) { + return false + } + + // If the touch surface is not attached or one of its ancestors is not visible, then we + // don't run the animation either. + if (!touchSurface.isAttachedToWindow) { + return false + } + + return (touchSurface.parent as? View)?.isShown ?: true + } + + internal fun onDozeAmountChanged(amount: Float) { + val alpha = Interpolators.ALPHA_OUT.getInterpolation(1 - amount) + val decorView = this.hostDialog.window?.decorView ?: return + if (decorView.hasOverlappingRendering() && alpha > 0.0f && + alpha < 1.0f && decorView.layerType != View.LAYER_TYPE_HARDWARE) { + decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null) + } + decorView.alpha = alpha + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt index b4ffb3f6cf4e..f7e0d588407f 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt @@ -1,7 +1,24 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.android.systemui.animation import android.graphics.Canvas import android.graphics.ColorFilter +import android.graphics.Insets import android.graphics.Matrix import android.graphics.PixelFormat import android.graphics.Rect @@ -42,6 +59,7 @@ open class GhostedViewLaunchAnimatorController( override var launchContainer = ghostedView.rootView as ViewGroup private val launchContainerOverlay: ViewGroupOverlay get() = launchContainer.overlay + private val launchContainerLocation = IntArray(2) /** The ghost view that is drawn and animated instead of the ghosted view. */ private var ghostView: GhostView? = null @@ -59,8 +77,12 @@ open class GhostedViewLaunchAnimatorController( * [backgroundView]. */ private var backgroundDrawable: WrappedDrawable? = null + private val backgroundInsets by lazy { getBackground()?.opticalInsets ?: Insets.NONE } private var startBackgroundAlpha: Int = 0xFF + private val ghostedViewLocation = IntArray(2) + private val ghostedViewState = LaunchAnimator.State() + /** * Return the background of the [ghostedView]. This background will be used to draw the * background of the background view that is expanding up to the final animation position. This @@ -103,16 +125,24 @@ open class GhostedViewLaunchAnimatorController( return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius } - override fun createAnimatorState(): ActivityLaunchAnimator.State { - val location = ghostedView.locationOnScreen - return ActivityLaunchAnimator.State( - top = location[1], - bottom = location[1] + ghostedView.height, - left = location[0], - right = location[0] + ghostedView.width, + override fun createAnimatorState(): LaunchAnimator.State { + val state = LaunchAnimator.State( topCornerRadius = getCurrentTopCornerRadius(), bottomCornerRadius = getCurrentBottomCornerRadius() ) + fillGhostedViewState(state) + return state + } + + fun fillGhostedViewState(state: LaunchAnimator.State) { + // For the animation we are interested in the area that has a non transparent background, + // so we have to take the optical insets into account. + ghostedView.getLocationOnScreen(ghostedViewLocation) + val insets = backgroundInsets + state.top = ghostedViewLocation[1] + insets.top + state.bottom = ghostedViewLocation[1] + ghostedView.height - insets.bottom + state.left = ghostedViewLocation[0] + insets.left + state.right = ghostedViewLocation[0] + ghostedView.width - insets.right } override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { @@ -144,7 +174,7 @@ open class GhostedViewLaunchAnimatorController( } override fun onLaunchAnimationProgress( - state: ActivityLaunchAnimator.State, + state: LaunchAnimator.State, progress: Float, linearProgress: Float ) { @@ -162,19 +192,47 @@ open class GhostedViewLaunchAnimatorController( return } - val scale = min(state.widthRatio, state.heightRatio) - ghostViewMatrix.setValues(initialGhostViewMatrixValues) - ghostViewMatrix.postScale(scale, scale, state.startCenterX, state.startCenterY) + // The ghost and backgrounds views were made invisible earlier. That can for instance happen + // when animating a dialog into a view. + if (ghostView.visibility == View.INVISIBLE) { + ghostView.visibility = View.VISIBLE + backgroundView.visibility = View.VISIBLE + } + + fillGhostedViewState(ghostedViewState) + val leftChange = state.left - ghostedViewState.left + val rightChange = state.right - ghostedViewState.right + val topChange = state.top - ghostedViewState.top + val bottomChange = state.bottom - ghostedViewState.bottom + + val widthRatio = state.width.toFloat() / ghostedViewState.width + val heightRatio = state.height.toFloat() / ghostedViewState.height + val scale = min(widthRatio, heightRatio) + + launchContainer.getLocationOnScreen(launchContainerLocation) + GhostView.calculateMatrix(ghostedView, launchContainer, ghostViewMatrix) + ghostViewMatrix.postScale( + scale, scale, + ghostedViewState.centerX - launchContainerLocation[0], + ghostedViewState.centerY - launchContainerLocation[1] + ) ghostViewMatrix.postTranslate( - (state.leftChange + state.rightChange) / 2f, - (state.topChange + state.bottomChange) / 2f + (leftChange + rightChange) / 2f, + (topChange + bottomChange) / 2f ) ghostView.animationMatrix = ghostViewMatrix - backgroundView.top = state.top - backgroundView.bottom = state.bottom - backgroundView.left = state.left - backgroundView.right = state.right + // We need to take into account the background insets for the background position. + val insets = backgroundInsets + val topWithInsets = state.top - insets.top + val leftWithInsets = state.left - insets.left + val rightWithInsets = state.right + insets.right + val bottomWithInsets = state.bottom + insets.bottom + + backgroundView.top = topWithInsets - launchContainerLocation[1] + backgroundView.bottom = bottomWithInsets - launchContainerLocation[1] + backgroundView.left = leftWithInsets - launchContainerLocation[0] + backgroundView.right = rightWithInsets - launchContainerLocation[0] val backgroundDrawable = backgroundDrawable!! backgroundDrawable.wrapped?.let { @@ -207,7 +265,7 @@ open class GhostedViewLaunchAnimatorController( * [drawable] is a [LayerDrawable], this will return the first layer that is a * [GradientDrawable]. */ - private fun findGradientDrawable(drawable: Drawable): GradientDrawable? { + fun findGradientDrawable(drawable: Drawable): GradientDrawable? { if (drawable is GradientDrawable) { return drawable } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java b/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java index 659b9fee8656..27658824933a 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java @@ -16,6 +16,7 @@ package com.android.systemui.animation; +import android.graphics.Path; import android.util.MathUtils; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateInterpolator; @@ -29,7 +30,97 @@ import android.view.animation.PathInterpolator; * Utility class to receive interpolators from */ public class Interpolators { - public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); + + /* + * ============================================================================================ + * Emphasized interpolators. + * ============================================================================================ + */ + + /** + * The default emphasized interpolator. Used for hero / emphasized movement of content. + */ + public static final Interpolator EMPHASIZED = createEmphasizedInterpolator(); + + /** + * The accelerated emphasized interpolator. Used for hero / emphasized movement of content that + * is disappearing e.g. when moving off screen. + */ + public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator( + 0.3f, 0f, 0.8f, 0.15f); + + /** + * The decelerating emphasized interpolator. Used for hero / emphasized movement of content that + * is appearing e.g. when coming from off screen + */ + public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator( + 0.05f, 0.7f, 0.1f, 1f); + + + /* + * ============================================================================================ + * Standard interpolators. + * ============================================================================================ + */ + + /** + * The standard interpolator that should be used on every normal animation + */ + public static final Interpolator STANDARD = new PathInterpolator( + 0.2f, 0f, 0f, 1f); + + /** + * The standard accelerating interpolator that should be used on every regular movement of + * content that is disappearing e.g. when moving off screen. + */ + public static final Interpolator STANDARD_ACCELERATE = new PathInterpolator( + 0.3f, 0f, 1f, 1f); + + /** + * The standard decelerating interpolator that should be used on every regular movement of + * content that is appearing e.g. when coming from off screen. + */ + public static final Interpolator STANDARD_DECELERATE = new PathInterpolator( + 0f, 0f, 0f, 1f); + + /* + * ============================================================================================ + * Legacy + * ============================================================================================ + */ + + /** + * The default legacy interpolator as defined in Material 1. Also known as FAST_OUT_SLOW_IN. + */ + public static final Interpolator LEGACY = new PathInterpolator(0.4f, 0f, 0.2f, 1f); + + /** + * The default legacy accelerating interpolator as defined in Material 1. + * Also known as FAST_OUT_LINEAR_IN. + */ + public static final Interpolator LEGACY_ACCELERATE = new PathInterpolator(0.4f, 0f, 1f, 1f); + + /** + * The default legacy decelerating interpolator as defined in Material 1. + * Also known as LINEAR_OUT_SLOW_IN. + */ + public static final Interpolator LEGACY_DECELERATE = new PathInterpolator(0f, 0f, 0.2f, 1f); + + /** + * Linear interpolator. Often used if the interpolator is for different properties who need + * different interpolations. + */ + public static final Interpolator LINEAR = new LinearInterpolator(); + + /* + * ============================================================================================ + * Custom interpolators + * ============================================================================================ + */ + + public static final Interpolator FAST_OUT_SLOW_IN = LEGACY; + public static final Interpolator FAST_OUT_LINEAR_IN = LEGACY_ACCELERATE; + public static final Interpolator LINEAR_OUT_SLOW_IN = LEGACY_DECELERATE; /** * Like {@link #FAST_OUT_SLOW_IN}, but used in case the animation is played in reverse (i.e. t @@ -37,12 +128,9 @@ public class Interpolators { */ public static final Interpolator FAST_OUT_SLOW_IN_REVERSE = new PathInterpolator(0.8f, 0f, 0.6f, 1f); - public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); - public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); public static final Interpolator SLOW_OUT_LINEAR_IN = new PathInterpolator(0.8f, 0f, 1f, 1f); public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f); - public static final Interpolator LINEAR = new LinearInterpolator(); public static final Interpolator ACCELERATE = new AccelerateInterpolator(); public static final Interpolator ACCELERATE_DECELERATE = new AccelerateDecelerateInterpolator(); public static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f); @@ -72,6 +160,12 @@ public class Interpolators { public static final Interpolator TOUCH_RESPONSE_REVERSE = new PathInterpolator(0.9f, 0f, 0.7f, 1f); + /* + * ============================================================================================ + * Functions / Utilities + * ============================================================================================ + */ + /** * Calculate the amount of overshoot using an exponential falloff function with desired * properties, where the overshoot smoothly transitions at the 1.0f boundary into the @@ -122,4 +216,14 @@ public class Interpolators { return (float) (1f - 0.5f * (1f - Math.cos(3.14159f * oneMinusFrac * oneMinusFrac))); } } + + // Create the default emphasized interpolator + private static PathInterpolator createEmphasizedInterpolator() { + Path path = new Path(); + // Doing the same as fast_out_extra_slow_in + path.moveTo(0f, 0f); + path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f); + path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f); + return new PathInterpolator(path); + } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt new file mode 100644 index 000000000000..3bf6c5ebd091 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.drawable.GradientDrawable +import android.util.Log +import android.util.MathUtils +import android.view.View +import android.view.ViewGroup +import android.view.animation.AnimationUtils +import android.view.animation.PathInterpolator +import kotlin.math.roundToInt + +private const val TAG = "LaunchAnimator" + +/** A base class to animate a window launch (activity or dialog) from a view . */ +class LaunchAnimator @JvmOverloads constructor( + context: Context, + private val isForTesting: Boolean = false +) { + companion object { + internal const val DEBUG = false + const val ANIMATION_DURATION = 500L + private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L + private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L + private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT + + private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f) + private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC) + + /** + * Given the [linearProgress] of a launch animation, return the linear progress of the + * sub-animation starting [delay] ms after the launch animation and that lasts [duration]. + */ + @JvmStatic + fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float { + return MathUtils.constrain( + (linearProgress * ANIMATION_DURATION - delay) / duration, + 0.0f, + 1.0f + ) + } + } + + /** The interpolator used for the width, height, Y position and corner radius. */ + private val animationInterpolator = AnimationUtils.loadInterpolator(context, + R.interpolator.launch_animation_interpolator_y) + + /** The interpolator used for the X position. */ + private val animationInterpolatorX = AnimationUtils.loadInterpolator(context, + R.interpolator.launch_animation_interpolator_x) + + private val launchContainerLocation = IntArray(2) + private val cornerRadii = FloatArray(8) + + /** + * A controller that takes care of applying the animation to an expanding view. + * + * Note that all callbacks (onXXX methods) are all called on the main thread. + */ + interface Controller { + /** + * The container in which the view that started the animation will be animating together + * with the opening window. + * + * This will be used to: + * - Get the associated [Context]. + * - Compute whether we are expanding fully above the launch container. + * - Apply surface transactions in sync with RenderThread when animating an activity + * launch. + * + * This container can be changed to force this [Controller] to animate the expanding view + * inside a different location, for instance to ensure correct layering during the + * animation. + */ + var launchContainer: ViewGroup + + /** + * Return the [State] of the view that will be animated. We will animate from this state to + * the final window state. + * + * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the + * animation. + */ + fun createAnimatorState(): State + + /** + * The animation started. This is typically used to initialize any additional resource + * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding + * fully above the [launchContainer]. + */ + fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {} + + /** The animation made progress and the expandable view [state] should be updated. */ + fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {} + + /** + * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was + * called previously. This is typically used to clean up the resources initialized when the + * animation was started. + */ + fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {} + } + + /** The state of an expandable view during a [LaunchAnimator] animation. */ + open class State( + /** The position of the view in screen space coordinates. */ + var top: Int = 0, + var bottom: Int = 0, + var left: Int = 0, + var right: Int = 0, + + var topCornerRadius: Float = 0f, + var bottomCornerRadius: Float = 0f + ) { + private val startTop = top + + val width: Int + get() = right - left + + val height: Int + get() = bottom - top + + open val topChange: Int + get() = top - startTop + + val centerX: Float + get() = left + width / 2f + + val centerY: Float + get() = top + height / 2f + + /** Whether the expanding view should be visible or hidden. */ + var visible: Boolean = true + } + + interface Animation { + /** Cancel the animation. */ + fun cancel() + } + + /** + * Start a launch animation controlled by [controller] towards [endState]. An intermediary + * layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and + * should be the same background color as the opening (or closing) window. If [drawHole] is + * true, then this intermediary layer will be drawn with SRC blending mode while it fades out. + * + * TODO(b/184121838): Remove [drawHole] and instead make the StatusBar draw this hole instead. + */ + fun startAnimation( + controller: Controller, + endState: State, + windowBackgroundColor: Int, + drawHole: Boolean = false + ): Animation { + val state = controller.createAnimatorState() + + // Start state. + val startTop = state.top + val startBottom = state.bottom + val startLeft = state.left + val startRight = state.right + val startCenterX = (startLeft + startRight) / 2f + val startWidth = startRight - startLeft + val startTopCornerRadius = state.topCornerRadius + val startBottomCornerRadius = state.bottomCornerRadius + + // End state. + var endTop = endState.top + var endBottom = endState.bottom + var endLeft = endState.left + var endRight = endState.right + var endCenterX = (endLeft + endRight) / 2f + var endWidth = endRight - endLeft + val endTopCornerRadius = endState.topCornerRadius + val endBottomCornerRadius = endState.bottomCornerRadius + + fun maybeUpdateEndState() { + if (endTop != endState.top || endBottom != endState.bottom || + endLeft != endState.left || endRight != endState.right) { + endTop = endState.top + endBottom = endState.bottom + endLeft = endState.left + endRight = endState.right + endCenterX = (endLeft + endRight) / 2f + endWidth = endRight - endLeft + } + } + + val launchContainer = controller.launchContainer + val isExpandingFullyAbove = isExpandingFullyAbove(launchContainer, endState) + + // We add an extra layer with the same color as the dialog/app splash screen background + // color, which is usually the same color of the app background. We first fade in this layer + // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the + // launch container and reveal the opening window. + val windowBackgroundLayer = GradientDrawable().apply { + setColor(windowBackgroundColor) + alpha = 0 + } + + // Update state. + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = if (isForTesting) 0 else ANIMATION_DURATION + animator.interpolator = Interpolators.LINEAR + + val launchContainerOverlay = launchContainer.overlay + var cancelled = false + animator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { + if (DEBUG) { + Log.d(TAG, "Animation started") + } + controller.onLaunchAnimationStart(isExpandingFullyAbove) + + // Add the drawable to the launch container overlay. Overlays always draw + // drawables after views, so we know that it will be drawn above any view added + // by the controller. + launchContainerOverlay.add(windowBackgroundLayer) + } + + override fun onAnimationEnd(animation: Animator?) { + if (DEBUG) { + Log.d(TAG, "Animation ended") + } + controller.onLaunchAnimationEnd(isExpandingFullyAbove) + launchContainerOverlay.remove(windowBackgroundLayer) + } + }) + + animator.addUpdateListener { animation -> + if (cancelled) { + // TODO(b/184121838): Cancel the animator directly instead of just skipping the + // update. + return@addUpdateListener + } + + maybeUpdateEndState() + + // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non + // reversed animation. + val linearProgress = animation.animatedFraction + val progress = animationInterpolator.getInterpolation(linearProgress) + val xProgress = animationInterpolatorX.getInterpolation(linearProgress) + + val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress) + val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f + + state.top = MathUtils.lerp(startTop, endTop, progress).roundToInt() + state.bottom = MathUtils.lerp(startBottom, endBottom, progress).roundToInt() + state.left = (xCenter - halfWidth).roundToInt() + state.right = (xCenter + halfWidth).roundToInt() + + state.topCornerRadius = + MathUtils.lerp(startTopCornerRadius, endTopCornerRadius, progress) + state.bottomCornerRadius = + MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress) + + // The expanding view can/should be hidden once it is completely covered by the opening + // window. + state.visible = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1 + + applyStateToWindowBackgroundLayer( + windowBackgroundLayer, + state, + linearProgress, + launchContainer, + drawHole + ) + controller.onLaunchAnimationProgress(state, progress, linearProgress) + } + + animator.start() + return object : Animation { + override fun cancel() { + cancelled = true + animator.cancel() + } + } + } + + /** Return whether we are expanding fully above the [launchContainer]. */ + internal fun isExpandingFullyAbove(launchContainer: View, endState: State): Boolean { + launchContainer.getLocationOnScreen(launchContainerLocation) + return endState.top <= launchContainerLocation[1] && + endState.bottom >= launchContainerLocation[1] + launchContainer.height && + endState.left <= launchContainerLocation[0] && + endState.right >= launchContainerLocation[0] + launchContainer.width + } + + private fun applyStateToWindowBackgroundLayer( + drawable: GradientDrawable, + state: State, + linearProgress: Float, + launchContainer: View, + drawHole: Boolean + ) { + // Update position. + launchContainer.getLocationOnScreen(launchContainerLocation) + drawable.setBounds( + state.left - launchContainerLocation[0], + state.top - launchContainerLocation[1], + state.right - launchContainerLocation[0], + state.bottom - launchContainerLocation[1] + ) + + // Update radius. + cornerRadii[0] = state.topCornerRadius + cornerRadii[1] = state.topCornerRadius + cornerRadii[2] = state.topCornerRadius + cornerRadii[3] = state.topCornerRadius + cornerRadii[4] = state.bottomCornerRadius + cornerRadii[5] = state.bottomCornerRadius + cornerRadii[6] = state.bottomCornerRadius + cornerRadii[7] = state.bottomCornerRadius + drawable.cornerRadii = cornerRadii + + // We first fade in the background layer to hide the expanding view, then fade it out + // with SRC mode to draw a hole punch in the status bar and reveal the opening window. + val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) + if (fadeInProgress < 1) { + val alpha = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(fadeInProgress) + drawable.alpha = (alpha * 0xFF).roundToInt() + } else { + val fadeOutProgress = getProgress( + linearProgress, ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW) + val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress) + drawable.alpha = (alpha * 0xFF).roundToInt() + + if (drawHole) { + drawable.setXfermode(SRC_MODE) + } + } + } +} diff --git a/packages/SystemUI/res-keyguard/color/numpad_key_color_secondary.xml b/packages/SystemUI/res-keyguard/color/numpad_key_color_secondary.xml new file mode 100644 index 000000000000..08c66a24348c --- /dev/null +++ b/packages/SystemUI/res-keyguard/color/numpad_key_color_secondary.xml @@ -0,0 +1,5 @@ +<?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/colorAccentSecondaryVariant"/> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/drawable/ic_backspace_24dp.xml b/packages/SystemUI/res-keyguard/drawable/ic_backspace_24dp.xml index dd35dd91f5d6..1eec8204a7f2 100644 --- a/packages/SystemUI/res-keyguard/drawable/ic_backspace_24dp.xml +++ b/packages/SystemUI/res-keyguard/drawable/ic_backspace_24dp.xml @@ -20,6 +20,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> <path - android:fillColor="?android:attr/colorBackground" + android:fillColor="?android:attr/textColorPrimaryInverse" android:pathData="M9,15.59L12.59,12L9,8.41L10.41,7L14,10.59L17.59,7L19,8.41L15.41,12L19,15.59L17.59,17L14,13.41L10.41,17L9,15.59zM21,6H8l-4.5,6L8,18h13V6M21,4c1.1,0 2,0.9 2,2v12c0,1.1 -0.9,2 -2,2H8c-0.63,0 -1.22,-0.3 -1.6,-0.8L1,12l5.4,-7.2C6.78,4.3 7.37,4 8,4H21L21,4z"/> </vector> diff --git a/packages/SystemUI/res-keyguard/drawable/ic_keyboard_tab_36dp.xml b/packages/SystemUI/res-keyguard/drawable/ic_keyboard_tab_36dp.xml index b844515f1088..2ad5e54eb92a 100644 --- a/packages/SystemUI/res-keyguard/drawable/ic_keyboard_tab_36dp.xml +++ b/packages/SystemUI/res-keyguard/drawable/ic_keyboard_tab_36dp.xml @@ -19,7 +19,8 @@ android:viewportHeight="36" android:viewportWidth="36" android:width="36sp"> - <path android:fillColor="?android:attr/colorBackground" + + <path android:fillColor="?android:attr/textColorPrimaryInverse" android:pathData="M17.59,13.41L21.17,17H7v2h14.17l-3.59,3.59L19,24l6,-6l-6,-6L17.59, 13.41zM26,12v12h2V12H26z"/> </vector> diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml index 624ee9f51b2a..6e89fb0169ab 100644 --- a/packages/SystemUI/res-keyguard/values/styles.xml +++ b/packages/SystemUI/res-keyguard/values/styles.xml @@ -17,7 +17,7 @@ */ --> -<resources> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Keyguard PIN pad styles --> <style name="Keyguard.TextView" parent="@android:style/Widget.DeviceDefault.TextView"> <item name="android:textSize">@dimen/kg_status_line_font_size</item> @@ -58,11 +58,11 @@ <item name="android:textColor">?android:attr/textColorPrimary</item> </style> <style name="NumPadKey.Delete"> - <item name="android:colorControlNormal">?android:attr/textColorSecondary</item> + <item name="android:colorControlNormal">@color/numpad_key_color_secondary</item> <item name="android:src">@drawable/ic_backspace_24dp</item> </style> <style name="NumPadKey.Enter"> - <item name="android:colorControlNormal">?android:attr/textColorSecondary</item> + <item name="android:colorControlNormal">@color/numpad_key_color_secondary</item> <item name="android:src">@drawable/ic_keyboard_tab_36dp</item> </style> <style name="Widget.TextView.NumPadKey.Klondike" diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_background.xml deleted file mode 100644 index 3ceb0f6ac06a..000000000000 --- a/packages/SystemUI/res/drawable/media_output_dialog_background.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2020 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> - -<inset xmlns:android="http://schemas.android.com/apk/res/android"> - <shape android:shape="rectangle"> - <corners android:radius="8dp" /> - <solid android:color="?android:attr/colorBackground" /> - </shape> -</inset> diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml index 918635d666fa..c1d7308be5a8 100644 --- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml +++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml @@ -19,7 +19,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/internet_connectivity_dialog" - android:layout_width="@dimen/internet_dialog_list_max_width" + android:layout_width="@dimen/large_dialog_width" android:layout_height="@dimen/internet_dialog_list_max_height" android:background="@drawable/internet_dialog_rounded_top_corner_background" android:orientation="vertical"> diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml index d996cee4b39e..b33889469f48 100644 --- a/packages/SystemUI/res/layout/media_output_dialog.xml +++ b/packages/SystemUI/res/layout/media_output_dialog.xml @@ -18,7 +18,7 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/media_output_dialog" - android:layout_width="match_parent" + android:layout_width="@dimen/large_dialog_width" android:layout_height="wrap_content" android:orientation="vertical"> diff --git a/packages/SystemUI/res/layout/media_view.xml b/packages/SystemUI/res/layout/media_view.xml index 566cd25e86a5..b546a9cbe90e 100644 --- a/packages/SystemUI/res/layout/media_view.xml +++ b/packages/SystemUI/res/layout/media_view.xml @@ -134,6 +134,7 @@ android:background="@drawable/qs_media_light_source" android:forceHasOverlappingRendering="false"> <LinearLayout + android:id="@+id/media_seamless_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minHeight="@dimen/qs_seamless_height" diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml index 4b6e58fbd1e5..18315f1dff42 100644 --- a/packages/SystemUI/res/layout/status_bar_expanded.xml +++ b/packages/SystemUI/res/layout/status_bar_expanded.xml @@ -25,12 +25,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/transparent"> - <FrameLayout - android:id="@+id/big_clock_container" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="gone" /> - <ViewStub android:id="@+id/keyguard_qs_user_switch_stub" android:layout="@layout/keyguard_qs_user_switch" diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index d921d4958090..85f8f0957a08 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -104,6 +104,6 @@ <!-- When split shade is used, this panel should be aligned to the top --> <dimen name="qs_detail_margin_top">0dp</dimen> - <!-- Internet panel related dimensions --> - <dimen name="internet_dialog_list_max_width">624dp</dimen> + <!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) --> + <dimen name="large_dialog_width">624dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 08778bf14c66..03c6fddb145c 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -70,7 +70,7 @@ <color name="keyguard_shadow_color">#B2000000</color> <!-- Color for the images in keyguard number pad buttons --> - <color name="keyguard_keypad_image_color">@android:color/background_light</color> + <color name="keyguard_keypad_image_color">?android:attr/textColorPrimaryInverse</color> <!-- Color for rounded background for activated user in keyguard user switcher --> <color name="kg_user_switcher_activated_background_color">#26000000</color> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 5e495f0cd1a6..73643c8d8684 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -706,4 +706,8 @@ <item>@drawable/rounded_corner_bottom</item> <item>@drawable/rounded_corner_bottom_secondary</item> </array> + + <!-- Flag to enable privacy dot views, it shall be true for normal case --> + <bool name="config_enablePrivacyDot">true</bool> + </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 70149267e3dd..9f257466ff28 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1559,6 +1559,13 @@ <!-- Location on the screen of the center of the fingerprint sensor. For devices with under display fingerprint sensors, this directly corresponds to the fingerprint sensor's location. For devices with sensors on the back of the device, this corresponds to the location on the + screen directly in front of the sensor. + By default, this is set to @null to use the horizontal center of the screen. --> + <dimen name="physical_fingerprint_sensor_center_screen_location_x">@null</dimen> + + <!-- Location on the screen of the center of the fingerprint sensor. For devices with under + display fingerprint sensors, this directly corresponds to the fingerprint sensor's location. + For devices with sensors on the back of the device, this corresponds to the location on the screen directly in front of the sensor. --> <dimen name="physical_fingerprint_sensor_center_screen_location_y">610px</dimen> @@ -1596,7 +1603,9 @@ <!-- Internet panel related dimensions --> <dimen name="internet_dialog_list_margin">12dp</dimen> <dimen name="internet_dialog_list_max_height">646dp</dimen> - <dimen name="internet_dialog_list_max_width">@dimen/match_parent</dimen> + + <!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) --> + <dimen name="large_dialog_width">@dimen/match_parent</dimen> <!-- Signal icon in internet dialog --> <dimen name="signal_strength_icon_size">24dp</dimen> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 93d60cce2915..6594216e4290 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -331,9 +331,6 @@ <style name="Animation.ShutdownUi" parent="@android:style/Animation.Toast"> </style> - <style name="Animation.MediaOutputDialog" parent="@android:style/Animation.InputMethod"> - </style> - <!-- Standard animations for hiding and showing the status bar. --> <style name="Animation.StatusBar"> </style> @@ -436,10 +433,6 @@ <item name="android:windowCloseOnTouchOutside">true</item> </style> - <style name="Theme.SystemUI.Dialog.MediaOutput"> - <item name="android:windowBackground">@drawable/media_output_dialog_background</item> - </style> - <style name="QSBorderlessButton"> <item name="android:padding">12dp</item> <item name="android:background">@drawable/qs_btn_borderless_rect</item> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 92d1bc4173d8..5969e9290c9c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -90,6 +90,7 @@ import androidx.annotation.Nullable; import androidx.lifecycle.Observer; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.widget.LockPatternUtils; import com.android.settingslib.WirelessUtils; import com.android.settingslib.fuelgauge.BatteryStatus; @@ -321,6 +322,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private boolean mIsDreaming; private final DevicePolicyManager mDevicePolicyManager; private final BroadcastDispatcher mBroadcastDispatcher; + private final InteractionJankMonitor mInteractionJankMonitor; private boolean mLogoutEnabled; // cached value to avoid IPCs private boolean mIsUdfpsEnrolled; @@ -1770,6 +1772,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab AuthController authController, TelephonyListenerManager telephonyListenerManager, FeatureFlags featureFlags, + InteractionJankMonitor interactionJankMonitor, @Nullable Vibrator vibrator) { mContext = context; mSubscriptionManager = SubscriptionManager.from(context); @@ -1778,6 +1781,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mStrongAuthTracker = new StrongAuthTracker(context, this::notifyStrongAuthStateChanged); mBackgroundExecutor = backgroundExecutor; mBroadcastDispatcher = broadcastDispatcher; + mInteractionJankMonitor = interactionJankMonitor; mRingerModeTracker = ringerModeTracker; mStatusBarStateController = statusBarStateController; mStatusBarStateController.addCallback(mStatusBarStateControllerListener); @@ -2637,7 +2641,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * Handle {@link #MSG_USER_SWITCH_COMPLETE} */ - private void handleUserSwitchComplete(int userId) { + @VisibleForTesting + void handleUserSwitchComplete(int userId) { Assert.isMainThread(); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); @@ -2645,6 +2650,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab cb.onUserSwitchComplete(userId); } } + mInteractionJankMonitor.end(InteractionJankMonitor.CUJ_USER_SWITCH); } /** diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java index c659bf786a45..f4ce643a085c 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java @@ -18,6 +18,7 @@ package com.android.keyguard; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; +import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; import android.graphics.drawable.VectorDrawable; @@ -26,8 +27,6 @@ import android.view.MotionEvent; import androidx.annotation.Nullable; -import com.android.systemui.R; - /** * Similar to the {@link NumPadKey}, but displays an image. */ @@ -92,7 +91,10 @@ public class NumPadButton extends AlphaOptimizedImageButton { public void reloadColors() { if (mAnimator != null) mAnimator.reloadColors(getContext()); - int imageColor = getContext().getColor(R.color.keyguard_keypad_image_color); + int[] customAttrs = {android.R.attr.textColorPrimaryInverse}; + TypedArray a = getContext().obtainStyledAttributes(customAttrs); + int imageColor = a.getColor(0, 0); + a.recycle(); ((VectorDrawable) getDrawable()).setTintList(ColorStateList.valueOf(imageColor)); } } diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 19d029bfd161..23a3f8d58f71 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -160,6 +160,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { private Handler mHandler; private boolean mPendingRotationChange; private boolean mIsRoundedCornerMultipleRadius; + private boolean mIsPrivacyDotEnabled; private int mStatusBarHeightPortrait; private int mStatusBarHeightLandscape; private Drawable mRoundedCornerDrawable; @@ -253,6 +254,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { mRotation = mContext.getDisplay().getRotation(); mDisplayUniqueId = mContext.getDisplay().getUniqueId(); mIsRoundedCornerMultipleRadius = isRoundedCornerMultipleRadius(mContext, mDisplayUniqueId); + mIsPrivacyDotEnabled = mContext.getResources().getBoolean(R.bool.config_enablePrivacyDot); mWindowManager = mContext.getSystemService(WindowManager.class); mDisplayManager = mContext.getSystemService(DisplayManager.class); updateRoundedCornerDrawable(); @@ -312,24 +314,24 @@ public class ScreenDecorations extends SystemUI implements Tunable { } private void setupDecorations() { - if (hasRoundedCorners() || shouldDrawCutout()) { + if (hasRoundedCorners() || shouldDrawCutout() || mIsPrivacyDotEnabled) { updateStatusBarHeight(); final DisplayCutout cutout = getCutout(); - final Rect[] bounds = cutout == null ? null : cutout.getBoundingRectsAll(); - int rotatedPos; for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) { - rotatedPos = getBoundPositionFromRotation(i, mRotation); - if ((bounds != null && !bounds[rotatedPos].isEmpty()) - || shouldShowRoundedCorner(i)) { - createOverlay(i); + if (shouldShowCutout(i, cutout) || shouldShowRoundedCorner(i, cutout) + || shouldShowPrivacyDot(i, cutout)) { + createOverlay(i, cutout); } else { removeOverlay(i); } } - // Overlays have been created, send the dots to the controller - //TODO: need a better way to do this - mDotViewController.initialize( - mTopLeftDot, mTopRightDot, mBottomLeftDot, mBottomRightDot); + + if (mIsPrivacyDotEnabled) { + // Overlays have been created, send the dots to the controller + //TODO: need a better way to do this + mDotViewController.initialize( + mTopLeftDot, mTopRightDot, mBottomLeftDot, mBottomRightDot); + } } else { removeAllOverlays(); } @@ -416,7 +418,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { mOverlays[pos] = null; } - private void createOverlay(@BoundsPosition int pos) { + private void createOverlay(@BoundsPosition int pos, @Nullable DisplayCutout cutout) { if (mOverlays == null) { mOverlays = new View[BOUNDS_POSITION_LENGTH]; } @@ -437,7 +439,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { mOverlays[pos].setAlpha(0); mOverlays[pos].setForceDarkAllowed(false); - updateView(pos); + updateView(pos, cutout); mWindowManager.addView(mOverlays[pos], getWindowLayoutParams(pos)); @@ -461,34 +463,19 @@ public class ScreenDecorations extends SystemUI implements Tunable { * Allow overrides for top/bottom positions */ private View overlayForPosition(@BoundsPosition int pos) { - switch (pos) { - case BOUNDS_POSITION_TOP: - case BOUNDS_POSITION_LEFT: - View top = LayoutInflater.from(mContext) - .inflate(R.layout.rounded_corners_top, null); - mTopLeftDot = top.findViewById(R.id.privacy_dot_left_container); - mTopRightDot = top.findViewById(R.id.privacy_dot_right_container); - return top; - case BOUNDS_POSITION_BOTTOM: - case BOUNDS_POSITION_RIGHT: - View bottom = LayoutInflater.from(mContext) - .inflate(R.layout.rounded_corners_bottom, null); - mBottomLeftDot = bottom.findViewById(R.id.privacy_dot_left_container); - mBottomRightDot = bottom.findViewById(R.id.privacy_dot_right_container); - return bottom; - default: - throw new IllegalArgumentException("Unknown bounds position"); - } + final int layoutId = (pos == BOUNDS_POSITION_LEFT || pos == BOUNDS_POSITION_TOP) + ? R.layout.rounded_corners_top : R.layout.rounded_corners_bottom; + return LayoutInflater.from(mContext).inflate(layoutId, null); } - private void updateView(@BoundsPosition int pos) { + private void updateView(@BoundsPosition int pos, @Nullable DisplayCutout cutout) { if (mOverlays == null || mOverlays[pos] == null) { return; } // update rounded corner view rotation - updateRoundedCornerView(pos, R.id.left); - updateRoundedCornerView(pos, R.id.right); + updateRoundedCornerView(pos, R.id.left, cutout); + updateRoundedCornerView(pos, R.id.right, cutout); updateRoundedCornerSize(mRoundedDefault, mRoundedDefaultTop, mRoundedDefaultBottom); updateRoundedCornerImageView(); @@ -496,6 +483,8 @@ public class ScreenDecorations extends SystemUI implements Tunable { if (mCutoutViews != null && mCutoutViews[pos] != null) { mCutoutViews[pos].setRotation(mRotation); } + + updatePrivacyDotView(pos, cutout); } @VisibleForTesting @@ -671,11 +660,12 @@ public class ScreenDecorations extends SystemUI implements Tunable { if (mOverlays != null) { updateLayoutParams(); + final DisplayCutout cutout = getCutout(); for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) { if (mOverlays[i] == null) { continue; } - updateView(i); + updateView(i, cutout); } } } @@ -807,13 +797,14 @@ public class ScreenDecorations extends SystemUI implements Tunable { return drawable; } - private void updateRoundedCornerView(@BoundsPosition int pos, int id) { + private void updateRoundedCornerView(@BoundsPosition int pos, int id, + @Nullable DisplayCutout cutout) { final View rounded = mOverlays[pos].findViewById(id); if (rounded == null) { return; } rounded.setVisibility(View.GONE); - if (shouldShowRoundedCorner(pos)) { + if (shouldShowRoundedCorner(pos, cutout)) { final int gravity = getRoundedCornerGravity(pos, id == R.id.left); ((FrameLayout.LayoutParams) rounded.getLayoutParams()).gravity = gravity; setRoundedCornerOrientation(rounded, gravity); @@ -821,6 +812,26 @@ public class ScreenDecorations extends SystemUI implements Tunable { } } + private void updatePrivacyDotView(@BoundsPosition int pos, @Nullable DisplayCutout cutout) { + final ViewGroup viewGroup = (ViewGroup) mOverlays[pos]; + + final View left = viewGroup.findViewById(R.id.privacy_dot_left_container); + final View right = viewGroup.findViewById(R.id.privacy_dot_right_container); + if (shouldShowPrivacyDot(pos, cutout)) { + // TODO (b/201481944) Privacy Dots pos and var are wrong with long side cutout enable + if (pos == BOUNDS_POSITION_LEFT || pos == BOUNDS_POSITION_TOP) { + mTopLeftDot = left; + mTopRightDot = right; + } else { + mBottomLeftDot = left; + mBottomRightDot = right; + } + } else { + viewGroup.removeView(left); + viewGroup.removeView(right); + } + } + private int getRoundedCornerGravity(@BoundsPosition int pos, boolean isStart) { final int rotatedPos = getBoundPositionFromRotation(pos, mRotation); switch (rotatedPos) { @@ -872,12 +883,8 @@ public class ScreenDecorations extends SystemUI implements Tunable { || mIsRoundedCornerMultipleRadius; } - private boolean shouldShowRoundedCorner(@BoundsPosition int pos) { - if (!hasRoundedCorners()) { - return false; - } - - DisplayCutout cutout = getCutout(); + private boolean isDefaultShownOverlayPos(@BoundsPosition int pos, + @Nullable DisplayCutout cutout) { // for cutout is null or cutout with only waterfall. final boolean emptyBoundsOrWaterfall = cutout == null || cutout.isBoundsEmpty(); // Shows rounded corner on left and right overlays only when there is no top or bottom @@ -892,6 +899,21 @@ public class ScreenDecorations extends SystemUI implements Tunable { } } + private boolean shouldShowRoundedCorner(@BoundsPosition int pos, + @Nullable DisplayCutout cutout) { + return hasRoundedCorners() && isDefaultShownOverlayPos(pos, cutout); + } + + private boolean shouldShowPrivacyDot(@BoundsPosition int pos, @Nullable DisplayCutout cutout) { + return mIsPrivacyDotEnabled && isDefaultShownOverlayPos(pos, cutout); + } + + private boolean shouldShowCutout(@BoundsPosition int pos, @Nullable DisplayCutout cutout) { + final Rect[] bounds = cutout == null ? null : cutout.getBoundingRectsAll(); + final int rotatedPos = getBoundPositionFromRotation(pos, mRotation); + return (bounds != null && !bounds[rotatedPos].isEmpty()); + } + private boolean shouldDrawCutout() { return shouldDrawCutout(mContext); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java index c32c1a2f3e53..48f6431aec69 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java @@ -16,7 +16,6 @@ package com.android.systemui.biometrics; -import android.annotation.NonNull; import android.content.Context; import android.graphics.drawable.Animatable2; import android.graphics.drawable.AnimatedVectorDrawable; @@ -30,6 +29,7 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; @@ -163,6 +163,18 @@ public class AuthBiometricFaceView extends AuthBiometricView { } @Nullable @VisibleForTesting IconController mFaceIconController; + @NonNull private final OnAttachStateChangeListener mOnAttachStateChangeListener = + new OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + + } + + @Override + public void onViewDetachedFromWindow(View v) { + mFaceIconController.deactivate(); + } + }; public AuthBiometricFaceView(Context context) { this(context, null); @@ -181,6 +193,8 @@ public class AuthBiometricFaceView extends AuthBiometricView { protected void onFinishInflate() { super.onFinishInflate(); mFaceIconController = new IconController(mContext, mIconView, mIndicatorView); + + addOnAttachStateChangeListener(mOnAttachStateChangeListener); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 71445a7c2cfe..f4b446b50c9e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -29,6 +29,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.PointF; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricConstants; @@ -97,7 +98,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, private final Provider<UdfpsController> mUdfpsControllerFactory; private final Provider<SidefpsController> mSidefpsControllerFactory; @Nullable private final PointF mFaceAuthSensorLocation; - @Nullable private final PointF mFingerprintLocation; + @Nullable private PointF mFingerprintLocation; private final Set<Callback> mCallbacks = new HashSet<>(); // TODO: These should just be saved from onSaveState @@ -481,9 +482,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, (float) faceAuthLocation[1]); } - mFingerprintLocation = new PointF(DisplayUtils.getWidth(mContext) / 2, - mContext.getResources().getDimensionPixelSize( - com.android.systemui.R.dimen.physical_fingerprint_sensor_center_screen_location_y)); + updateFingerprintLocation(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); @@ -491,6 +490,21 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, context.registerReceiver(mBroadcastReceiver, filter); } + private void updateFingerprintLocation() { + int xLocation = DisplayUtils.getWidth(mContext) / 2; + try { + xLocation = mContext.getResources().getDimensionPixelSize( + com.android.systemui.R.dimen + .physical_fingerprint_sensor_center_screen_location_x); + } catch (Resources.NotFoundException e) { + } + int yLocation = mContext.getResources().getDimensionPixelSize( + com.android.systemui.R.dimen.physical_fingerprint_sensor_center_screen_location_y); + mFingerprintLocation = new PointF( + xLocation, + yLocation); + } + @SuppressWarnings("deprecation") @Override public void start() { @@ -767,6 +781,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); + updateFingerprintLocation(); // Save the state of the current dialog (buttons showing, etc) if (mCurrentDialog != null) { @@ -796,6 +811,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } private void onOrientationChanged() { + updateFingerprintLocation(); if (mCurrentDialog != null) { mCurrentDialog.onOrientationChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index ba64195ea78b..eb6b193d85a3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.res.Configuration import android.graphics.PointF import android.hardware.biometrics.BiometricSourceType +import android.util.DisplayMetrics import android.util.Log import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor @@ -45,6 +46,7 @@ import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.util.leak.RotationUtils private const val WAKE_AND_UNLOCK_FADE_DURATION = 180L @@ -182,7 +184,7 @@ class AuthRippleController @Inject constructor( } fun updateSensorLocation() { - fingerprintSensorLocation = authController.fingerprintSensorLocation + updateFingerprintLocation() faceSensorLocation = authController.faceAuthSensorLocation fingerprintSensorLocation?.let { circleReveal = CircleReveal( @@ -197,6 +199,35 @@ class AuthRippleController @Inject constructor( } } + private fun updateFingerprintLocation() { + val displayMetrics = DisplayMetrics() + sysuiContext.display?.getRealMetrics(displayMetrics) + val width = displayMetrics.widthPixels + val height = displayMetrics.heightPixels + + authController.fingerprintSensorLocation?.let { + fingerprintSensorLocation = when (RotationUtils.getRotation(sysuiContext)) { + RotationUtils.ROTATION_LANDSCAPE -> { + val normalizedYPos: Float = it.y / width + val normalizedXPos: Float = it.x / height + PointF(width * normalizedYPos, height * (1 - normalizedXPos)) + } + RotationUtils.ROTATION_UPSIDE_DOWN -> { + PointF(width - it.x, height - it.y) + } + RotationUtils.ROTATION_SEASCAPE -> { + val normalizedYPos: Float = it.y / width + val normalizedXPos: Float = it.x / height + PointF(width * (1 - normalizedYPos), height * normalizedXPos) + } + else -> { + // ROTATION_NONE + PointF(it.x, it.y) + } + } + } + } + private fun updateRippleColor() { mView.setColor( Utils.getColorAttr(sysuiContext, android.R.attr.colorAccent).defaultColor) @@ -314,10 +345,12 @@ class AuthRippleController @Inject constructor( } } "fingerprint" -> { + updateSensorLocation() pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation") showRipple(BiometricSourceType.FINGERPRINT) } "face" -> { + updateSensorLocation() pw.println("face ripple sensorLocation=$faceSensorLocation") showRipple(BiometricSourceType.FACE) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 4d1608fb445d..d74df37401d0 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -74,6 +74,7 @@ import android.view.inputmethod.InputMethodManager; import com.android.internal.app.IBatteryStats; import com.android.internal.appwidget.IAppWidgetService; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.util.LatencyTracker; import com.android.systemui.dagger.qualifiers.DisplayId; @@ -220,6 +221,12 @@ public class FrameworkServicesModule { @Provides @Singleton + static InteractionJankMonitor provideInteractionJankMonitor() { + return InteractionJankMonitor.getInstance(); + } + + @Provides + @Singleton static InputMethodManager provideInputMethodManager(Context context) { return context.getSystemService(InputMethodManager.class); } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index e7445f920ffe..424f80177d9c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -364,9 +364,9 @@ public class MediaControlPanel { seamlessView.setVisibility(View.VISIBLE); setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */); setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */); - seamlessView.setOnClickListener(v -> { - mMediaOutputDialogFactory.create(data.getPackageName(), true); - }); + seamlessView.setOnClickListener( + v -> mMediaOutputDialogFactory.create(data.getPackageName(), true, + mPlayerViewHolder.getSeamlessButton())); ImageView iconView = mPlayerViewHolder.getSeamlessIcon(); TextView deviceName = mPlayerViewHolder.getSeamlessText(); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index eacdab6e537d..06a1eeac425d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -889,7 +889,7 @@ class MediaDataManager( dismissIntent = target .baseAction .extras - .getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent + .getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? } packageName(target)?.let { return SmartspaceMediaData(target.smartspaceTargetId, isActive, true, it, diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt index 35603b6ef6cc..f32dad632721 100644 --- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt @@ -43,6 +43,7 @@ class PlayerViewHolder private constructor(itemView: View) { val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless) val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image) val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text) + val seamlessButton = itemView.requireViewById<View>(R.id.media_seamless_button) // Seek bar val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar) diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index 391dff634dab..d1b6548132a6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -45,11 +45,14 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { private static final String TAG = "MediaOutputAdapter"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final MediaOutputDialog mMediaOutputDialog; private ViewGroup mConnectedItem; private boolean mIncludeDynamicGroup; - public MediaOutputAdapter(MediaOutputController controller) { + public MediaOutputAdapter(MediaOutputController controller, + MediaOutputDialog mediaOutputDialog) { super(controller); + mMediaOutputDialog = mediaOutputDialog; } @Override @@ -136,7 +139,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { mDivider.setTransitionAlpha(1); mAddIcon.setVisibility(View.VISIBLE); mAddIcon.setTransitionAlpha(1); - mAddIcon.setOnClickListener(v -> onEndItemClick()); + mAddIcon.setOnClickListener(this::onEndItemClick); } else { // Init non-active device layout mDivider.setVisibility(View.GONE); @@ -197,7 +200,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { mDivider.setTransitionAlpha(1); mAddIcon.setVisibility(View.VISIBLE); mAddIcon.setTransitionAlpha(1); - mAddIcon.setOnClickListener(v -> onEndItemClick()); + mAddIcon.setOnClickListener(this::onEndItemClick); } else { mDivider.setVisibility(View.GONE); mAddIcon.setVisibility(View.GONE); @@ -232,8 +235,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } } - private void onEndItemClick() { - mController.launchMediaOutputGroupDialog(); + private void onEndItemClick(View view) { + mController.launchMediaOutputGroupDialog(mMediaOutputDialog.getDialogView()); } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index cdcdf9a1d4de..85d0802012ac 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -82,7 +82,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements }; public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) { - super(context, R.style.Theme_SystemUI_Dialog_MediaOutput); + super(context); mContext = context; mMediaOutputController = mediaOutputController; mLayoutManager = new LinearLayoutManager(mContext); @@ -97,15 +97,15 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null); final Window window = getWindow(); final WindowManager.LayoutParams lp = window.getAttributes(); - lp.gravity = Gravity.BOTTOM; + lp.gravity = Gravity.CENTER; // Config insets to make sure the layout is above the navigation bar lp.setFitInsetsTypes(statusBars() | navigationBars()); lp.setFitInsetsSides(WindowInsets.Side.all()); lp.setFitInsetsIgnoringVisibility(true); window.setAttributes(lp); window.setContentView(mDialogView); - window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - window.setWindowAnimations(R.style.Animation_MediaOutputDialog); + window.setLayout(mContext.getResources().getDimensionPixelSize(R.dimen.large_dialog_width), + ViewGroup.LayoutParams.WRAP_CONTENT); mHeaderTitle = mDialogView.requireViewById(R.id.header_title); mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle); @@ -229,4 +229,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements void onHeaderIconClick() { } + + View getDialogView() { + return mDialogView; + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index b2def7a8596a..437a0c85a6e0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -32,6 +32,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.text.TextUtils; import android.util.Log; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -48,6 +49,7 @@ import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.MediaOutputConstants; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.R; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -73,6 +75,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { private final MediaSessionManager mMediaSessionManager; private final ShadeController mShadeController; private final ActivityStarter mActivityStarter; + private final DialogLaunchAnimator mDialogLaunchAnimator; private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>(); private final boolean mAboveStatusbar; private final NotificationEntryManager mNotificationEntryManager; @@ -82,6 +85,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { private MediaController mMediaController; @VisibleForTesting Callback mCallback; + Callback mPreviousCallback; @VisibleForTesting LocalMediaManager mLocalMediaManager; @@ -92,7 +96,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { public MediaOutputController(@NonNull Context context, String packageName, boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager lbm, ShadeController shadeController, ActivityStarter starter, - NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger) { + NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger, + DialogLaunchAnimator dialogLaunchAnimator) { mContext = context; mPackageName = packageName; mMediaSessionManager = mediaSessionManager; @@ -104,6 +109,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); mUiEventLogger = uiEventLogger; + mDialogLaunchAnimator = dialogLaunchAnimator; } void start(@NonNull Callback cb) { @@ -129,7 +135,19 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { } return; } + + if (mPreviousCallback != null) { + Log.w(TAG, + "Callback started when mPreviousCallback is not null, which is unexpected"); + mPreviousCallback.dismissDialog(); + } + + // If we start the output group dialog when the output dialog is shown, we need to keep a + // reference to the output dialog to set it back as the callback once we dismiss the output + // group dialog. + mPreviousCallback = mCallback; mCallback = cb; + mLocalMediaManager.unregisterCallback(this); mLocalMediaManager.stopScan(); mLocalMediaManager.registerCallback(this); @@ -145,6 +163,15 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { mLocalMediaManager.stopScan(); } mMediaDevices.clear(); + + // If there was a previous callback, i.e. we just dismissed the output group dialog and are + // now back on the output dialog, then we reset the callback to its previous value. + mCallback = null; + Callback previous = mPreviousCallback; + mPreviousCallback = null; + if (previous != null) { + start(previous); + } } @Override @@ -436,6 +463,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { } void launchBluetoothPairing() { + // Dismissing a dialog into its touch surface and starting an activity at the same time + // looks bad, so let's make sure the dialog just fades out quickly. + mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations(); + mCallback.dismissDialog(); final ActivityStarter.OnDismissAction postKeyguardAction = () -> { mContext.sendBroadcast(new Intent() @@ -447,14 +478,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true); } - void launchMediaOutputDialog() { - mCallback.dismissDialog(); - new MediaOutputDialog(mContext, mAboveStatusbar, this, mUiEventLogger); - } - - void launchMediaOutputGroupDialog() { - mCallback.dismissDialog(); - new MediaOutputGroupDialog(mContext, mAboveStatusbar, this); + void launchMediaOutputGroupDialog(View mediaOutputDialog) { + // We show the output group dialog from the output dialog. + MediaOutputGroupDialog dialog = new MediaOutputGroupDialog(mContext, mAboveStatusbar, this); + mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog); } boolean isActiveRemoteDevice(@NonNull MediaDevice device) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java index 53029bd04ef6..eca8ac90427b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -40,11 +40,10 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { mediaOutputController, UiEventLogger uiEventLogger) { super(context, mediaOutputController); mUiEventLogger = uiEventLogger; - mAdapter = new MediaOutputAdapter(mMediaOutputController); + mAdapter = new MediaOutputAdapter(mMediaOutputController, this); if (!aboveStatusbar) { getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); } - show(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt index 0f340a5cedaa..b91901de5af3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt @@ -18,8 +18,10 @@ package com.android.systemui.media.dialog import android.content.Context import android.media.session.MediaSessionManager +import android.view.View import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.notification.NotificationEntryManager import com.android.systemui.statusbar.phone.ShadeController @@ -35,19 +37,29 @@ class MediaOutputDialogFactory @Inject constructor( private val shadeController: ShadeController, private val starter: ActivityStarter, private val notificationEntryManager: NotificationEntryManager, - private val uiEventLogger: UiEventLogger + private val uiEventLogger: UiEventLogger, + private val dialogLaunchAnimator: DialogLaunchAnimator ) { companion object { var mediaOutputDialog: MediaOutputDialog? = null } /** Creates a [MediaOutputDialog] for the given package. */ - fun create(packageName: String, aboveStatusBar: Boolean) { + fun create(packageName: String, aboveStatusBar: Boolean, view: View? = null) { + // Dismiss the previous dialog, if any. mediaOutputDialog?.dismiss() - mediaOutputDialog = MediaOutputController(context, packageName, aboveStatusBar, - mediaSessionManager, lbm, shadeController, starter, notificationEntryManager, - uiEventLogger).run { - MediaOutputDialog(context, aboveStatusBar, this, uiEventLogger) + + val controller = MediaOutputController(context, packageName, aboveStatusBar, + mediaSessionManager, lbm, shadeController, starter, notificationEntryManager, + uiEventLogger, dialogLaunchAnimator) + val dialog = MediaOutputDialog(context, aboveStatusBar, controller, uiEventLogger) + mediaOutputDialog = dialog + + // Show the dialog. + if (view != null) { + dialogLaunchAnimator.showFromView(dialog, view) + } else { + dialog.show() } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java index 407930492fbe..1300400f3b66 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupDialog.java @@ -38,7 +38,6 @@ public class MediaOutputGroupDialog extends MediaOutputBaseDialog { if (!aboveStatusbar) { getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); } - show(); } @Override @@ -83,6 +82,8 @@ public class MediaOutputGroupDialog extends MediaOutputBaseDialog { @Override void onHeaderIconClick() { - mMediaOutputController.launchMediaOutputDialog(); + // Given that we launched the media output group dialog from the media output dialog, + // dismissing this dialog will show the media output dialog again. + dismiss(); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java index dc54e1b52f2e..11430d93106a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java @@ -206,7 +206,7 @@ public class InternetDialog extends SystemUIDialog implements window.setContentView(mDialogView); //Only fix the width for large screen or tablet. window.setLayout(mContext.getResources().getDimensionPixelSize( - R.dimen.internet_dialog_list_max_width), ViewGroup.LayoutParams.WRAP_CONTENT); + R.dimen.large_dialog_width), ViewGroup.LayoutParams.WRAP_CONTENT); window.setWindowAnimations(R.style.Animation_InternetDialog); window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); window.addFlags(FLAG_LAYOUT_NO_LIMITS); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index 94f186f00778..ea00d920c9b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -22,6 +22,9 @@ import android.content.Context; import android.os.Handler; import com.android.internal.statusbar.IStatusBarService; +import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.animation.LaunchAnimator; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; @@ -62,6 +65,7 @@ import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl; import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; +import com.android.systemui.statusbar.phone.SystemUIHostDialogProvider; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger; import com.android.systemui.statusbar.policy.RemoteInputUriController; @@ -261,4 +265,29 @@ public interface StatusBarDependenciesModule { @Binds QSCarrierGroupController.SlotIndexResolver provideSlotIndexResolver( QSCarrierGroupController.SubscriptionManagerSlotIndexResolver impl); + + /** + */ + @Provides + @SysUISingleton + static LaunchAnimator provideLaunchAnimator(Context context) { + return new LaunchAnimator(context); + } + + /** + */ + @Provides + @SysUISingleton + static ActivityLaunchAnimator provideActivityLaunchAnimator(LaunchAnimator launchAnimator) { + return new ActivityLaunchAnimator(launchAnimator); + } + + /** + */ + @Provides + @SysUISingleton + static DialogLaunchAnimator provideDialogLaunchAnimator(Context context, + LaunchAnimator launchAnimator) { + return new DialogLaunchAnimator(context, launchAnimator, new SystemUIHostDialogProvider()); + } } 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 a76389b1bbc8..bdade2c6c2c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -86,6 +86,7 @@ class LockscreenSmartspaceController @Inject constructor( var stateChangeListener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { smartspaceViews.add(v as SmartspaceView) + connectSession() updateTextColorFromWallpaper() statusBarStateListener.onDozeAmountChanged(0f, statusBarStateController.dozeAmount) @@ -93,6 +94,10 @@ class LockscreenSmartspaceController @Inject constructor( override fun onViewDetachedFromWindow(v: View) { smartspaceViews.remove(v as SmartspaceView) + + if (smartspaceViews.isEmpty()) { + disconnect() + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt index f19cf5d8d9c7..64a73054c434 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt @@ -2,8 +2,8 @@ package com.android.systemui.statusbar.notification import android.util.MathUtils import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Interpolators +import com.android.systemui.animation.LaunchAnimator import kotlin.math.min /** Parameters for the notifications expand animations. */ @@ -15,7 +15,7 @@ class ExpandAnimationParameters( topCornerRadius: Float = 0f, bottomCornerRadius: Float = 0f -) : ActivityLaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) { +) : LaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) { @VisibleForTesting constructor() : this( top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f @@ -55,6 +55,6 @@ class ExpandAnimationParameters( } fun getProgress(delay: Long, duration: Long): Float { - return ActivityLaunchAnimator.getProgress(linearProgress, delay, duration) + return LaunchAnimator.getProgress(linearProgress, delay, duration) } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java index a9ad0005973f..60f44a0d4fca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java @@ -305,9 +305,6 @@ public class NotificationEntryManager implements NotificationEntry entry = mPendingNotifications.get(key); entry.abortTask(); mPendingNotifications.remove(key); - for (NotifCollectionListener listener : mNotifCollectionListeners) { - listener.onEntryCleanUp(entry); - } mLogger.logInflationAborted(key, "pending", reason); } NotificationEntry addedEntry = getActiveNotificationUnfiltered(key); @@ -477,6 +474,18 @@ public class NotificationEntryManager implements if (!lifetimeExtended) { // At this point, we are guaranteed the notification will be removed abortExistingInflation(key, "removeNotification"); + // Fix for b/201097913: NotifCollectionListener#onEntryRemoved specifies that + // #onEntryRemoved should be called when a notification is cancelled, + // regardless of whether the notification was pending or active. + // Note that mNotificationEntryListeners are NOT notified of #onEntryRemoved + // because for that interface, #onEntryRemoved should only be called for + // active entries, NOT pending ones. + for (NotifCollectionListener listener : mNotifCollectionListeners) { + listener.onEntryRemoved(pendingEntry, REASON_UNKNOWN); + } + for (NotifCollectionListener listener : mNotifCollectionListeners) { + listener.onEntryCleanUp(pendingEntry); + } mAllNotifications.remove(pendingEntry); mLeakDetector.trackGarbage(pendingEntry); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt index 1bbef2562d21..22c3eda03b1e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt @@ -3,6 +3,7 @@ package com.android.systemui.statusbar.notification import android.view.ViewGroup import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.LaunchAnimator import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.phone.HeadsUpManagerPhone @@ -54,7 +55,7 @@ class NotificationLaunchAnimatorController( // Do nothing. Notifications are always animated inside their rootView. } - override fun createAnimatorState(): ActivityLaunchAnimator.State { + override fun createAnimatorState(): LaunchAnimator.State { // If the notification panel is collapsed, the clip may be larger than the height. val height = max(0, notification.actualHeight - notification.clipBottomAmount) val location = notification.locationOnScreen @@ -72,12 +73,12 @@ class NotificationLaunchAnimatorController( notification.currentBackgroundRadiusTop } val params = ExpandAnimationParameters( - top = windowTop, - bottom = location[1] + height, - left = location[0], - right = location[0] + notification.width, - topCornerRadius = topCornerRadius, - bottomCornerRadius = notification.currentBackgroundRadiusBottom + top = windowTop, + bottom = location[1] + height, + left = location[0], + right = location[0] + notification.width, + topCornerRadius = topCornerRadius, + bottomCornerRadius = notification.currentBackgroundRadiusBottom ) params.startTranslationZ = notification.translationZ @@ -86,8 +87,8 @@ class NotificationLaunchAnimatorController( params.startClipTopAmount = notification.clipTopAmount if (notification.isChildInGroup) { params.startNotificationTop += notification.notificationParent.translationY - val parentRoundedClip = Math.max(clipStartLocation - - notification.notificationParent.locationOnScreen[1], 0) + val parentRoundedClip = Math.max( + clipStartLocation - notification.notificationParent.locationOnScreen[1], 0) params.parentStartRoundedTopClipping = parentRoundedClip val parentClip = notification.notificationParent.clipTopAmount @@ -157,7 +158,7 @@ class NotificationLaunchAnimatorController( } override fun onLaunchAnimationProgress( - state: ActivityLaunchAnimator.State, + state: LaunchAnimator.State, progress: Float, linearProgress: Float ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 5f44a7d5d870..fe154d232a88 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -72,6 +72,7 @@ public class KeyguardStatusBarView extends RelativeLayout { private boolean mKeyguardUserSwitcherEnabled; private final UserManager mUserManager; + private boolean mIsPrivacyDotEnabled; private int mSystemIconsSwitcherHiddenExpandedMargin; private int mStatusBarPaddingEnd; private int mMinDotWidth; @@ -112,7 +113,7 @@ public class KeyguardStatusBarView extends RelativeLayout { mCutoutSpace = findViewById(R.id.cutout_space_view); mStatusIconArea = findViewById(R.id.status_icon_area); mStatusIconContainer = findViewById(R.id.statusIcons); - + mIsPrivacyDotEnabled = mContext.getResources().getBoolean(R.bool.config_enablePrivacyDot); loadDimens(); } @@ -270,9 +271,10 @@ public class KeyguardStatusBarView extends RelativeLayout { mDisplayCutout, cornerCutoutMargins, mRoundedCornerPadding); // consider privacy dot space - final int minLeft = isLayoutRtl() ? Math.max(mMinDotWidth, mPadding.first) : mPadding.first; - final int minRight = isLayoutRtl() ? mPadding.second : - Math.max(mMinDotWidth, mPadding.second); + final int minLeft = (isLayoutRtl() && mIsPrivacyDotEnabled) + ? Math.max(mMinDotWidth, mPadding.first) : mPadding.first; + final int minRight = (!isLayoutRtl() && mIsPrivacyDotEnabled) + ? Math.max(mMinDotWidth, mPadding.second) : mPadding.second; setPadding(minLeft, waterfallTop, minRight, 0); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 9018cef72f7d..4862d1617837 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -103,8 +103,8 @@ import com.android.keyguard.dagger.KeyguardUserSwitcherComponent; import com.android.systemui.DejankUtils; import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.Interpolators; +import com.android.systemui.animation.LaunchAnimator; import com.android.systemui.biometrics.AuthController; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; @@ -212,7 +212,7 @@ public class NotificationPanelViewController extends PanelViewController { */ private static final int FLING_HIDE = 2; private static final long ANIMATION_DELAY_ICON_FADE_IN = - ActivityLaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION + LaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION - CollapsedStatusBarFragment.FADE_IN_DELAY - 48; private final DozeParameters mDozeParameters; @@ -305,7 +305,6 @@ public class NotificationPanelViewController extends PanelViewController { private KeyguardUserSwitcherController mKeyguardUserSwitcherController; private KeyguardStatusBarView mKeyguardStatusBar; private KeyguardStatusBarViewController mKeyguardStatusBarViewController; - private ViewGroup mBigClockContainer; @VisibleForTesting QS mQs; private FrameLayout mQsFrame; private KeyguardStatusViewController mKeyguardStatusViewController; @@ -334,6 +333,7 @@ public class NotificationPanelViewController extends PanelViewController { private boolean mKeyguardUserSwitcherEnabled; private boolean mDozing; private boolean mDozingOnDown; + private boolean mBouncerShowing; private int mBarState; private float mInitialHeightOnTouch; private float mInitialTouchX; @@ -404,7 +404,7 @@ public class NotificationPanelViewController extends PanelViewController { private Runnable mHeadsUpExistenceChangedRunnable = () -> { setHeadsUpAnimatingAway(false); - notifyBarPanelExpansionChanged(); + updatePanelExpansionAndVisibility(); }; // TODO (b/162832756): once migrated to the new pipeline, delete legacy group manager private NotificationGroupManagerLegacy mGroupManager; @@ -801,7 +801,6 @@ public class NotificationPanelViewController extends PanelViewController { private void onFinishInflate() { loadDimens(); mKeyguardStatusBar = mView.findViewById(R.id.keyguard_header); - mBigClockContainer = mView.findViewById(R.id.big_clock_container); FrameLayout userAvatarContainer = null; KeyguardUserSwitcherView keyguardUserSwitcherView = null; @@ -1084,7 +1083,6 @@ public class NotificationPanelViewController extends PanelViewController { R.layout.keyguard_user_switcher /* layoutId */, showKeyguardUserSwitcher /* enabled */); - mBigClockContainer.removeAllViews(); updateViewControllers(mView.findViewById(R.id.keyguard_status_view), userAvatarView, keyguardUserSwitcherView); @@ -1256,8 +1254,10 @@ public class NotificationPanelViewController extends PanelViewController { boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled(); final boolean hasVisibleNotifications = mNotificationStackScrollLayoutController .getVisibleNotificationCount() != 0 || mMediaDataManager.hasActiveMedia(); + boolean splitShadeWithActiveMedia = + mShouldUseSplitNotificationShade && mMediaDataManager.hasActiveMedia(); if ((hasVisibleNotifications && !mShouldUseSplitNotificationShade) - || (mShouldUseSplitNotificationShade && mMediaDataManager.hasActiveMedia())) { + || (splitShadeWithActiveMedia && !mDozing)) { mKeyguardStatusViewController.displayClock(SMALL); } else { mKeyguardStatusViewController.displayClock(LARGE); @@ -1319,7 +1319,8 @@ public class NotificationPanelViewController extends PanelViewController { private void updateKeyguardStatusViewAlignment(boolean animate) { boolean hasVisibleNotifications = mNotificationStackScrollLayoutController .getVisibleNotificationCount() != 0 || mMediaDataManager.hasActiveMedia(); - boolean shouldBeCentered = !mShouldUseSplitNotificationShade || !hasVisibleNotifications; + boolean shouldBeCentered = + !mShouldUseSplitNotificationShade || !hasVisibleNotifications || mDozing; if (mStatusViewCentered != shouldBeCentered) { mStatusViewCentered = shouldBeCentered; ConstraintSet constraintSet = new ConstraintSet(); @@ -2180,7 +2181,6 @@ public class NotificationPanelViewController extends PanelViewController { if (mBarState == StatusBarState.SHADE_LOCKED || mBarState == KEYGUARD) { updateKeyguardBottomAreaAlpha(); positionClockAndNotifications(); - updateBigClockAlpha(); } if (mAccessibilityManager.isEnabled()) { @@ -2953,20 +2953,6 @@ public class NotificationPanelViewController extends PanelViewController { mLockIconViewController.setAlpha(alpha); } - /** - * Custom clock fades away when user drags up to unlock or pulls down quick settings. - * - * Updates alpha of custom clock to match the alpha of the KeyguardBottomArea. See - * {@link #updateKeyguardBottomAreaAlpha}. - */ - private void updateBigClockAlpha() { - float expansionAlpha = MathUtils.map( - isUnlockHintRunning() ? 0 : KeyguardBouncer.ALPHA_EXPANSION_THRESHOLD, 1f, 0f, 1f, - getExpandedFraction()); - float alpha = Math.min(expansionAlpha, 1 - computeQsExpansionFraction()); - mBigClockContainer.setAlpha(alpha); - } - @Override protected void onExpandingStarted() { super.onExpandingStarted(); @@ -3228,11 +3214,19 @@ public class NotificationPanelViewController extends PanelViewController { public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { mHeadsUpAnimatingAway = headsUpAnimatingAway; mNotificationStackScrollLayoutController.setHeadsUpAnimatingAway(headsUpAnimatingAway); - updateHeadsUpVisibility(); + updateVisibility(); } - private void updateHeadsUpVisibility() { - ((PhoneStatusBarView) mBar).setHeadsUpVisible(mHeadsUpAnimatingAway || mHeadsUpPinnedMode); + /** Set whether the bouncer is showing. */ + public void setBouncerShowing(boolean bouncerShowing) { + mBouncerShowing = bouncerShowing; + updateVisibility(); + } + + @Override + protected boolean shouldPanelBeVisible() { + boolean headsUpVisible = mHeadsUpAnimatingAway || mHeadsUpPinnedMode; + return headsUpVisible || isExpanded() || mBouncerShowing; } @Override @@ -3316,7 +3310,6 @@ public class NotificationPanelViewController extends PanelViewController { } mNotificationStackScrollLayoutController.setExpandedHeight(expandedHeight); updateKeyguardBottomAreaAlpha(); - updateBigClockAlpha(); updateStatusBarIcons(); } @@ -3594,7 +3587,7 @@ public class NotificationPanelViewController extends PanelViewController { } public void applyLaunchAnimationProgress(float linearProgress) { - boolean hideIcons = ActivityLaunchAnimator.getProgress(linearProgress, + boolean hideIcons = LaunchAnimator.getProgress(linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f; if (hideIcons != mHideIconsDuringLaunchAnimation) { mHideIconsDuringLaunchAnimation = hideIcons; @@ -3642,6 +3635,11 @@ public class NotificationPanelViewController extends PanelViewController { } } + /** */ + public void setImportantForAccessibility(int mode) { + mView.setImportantForAccessibility(mode); + } + /** * Do not let the user drag the shade up and down for the current touch session. * This is necessary to avoid shade expansion while/after the bouncer is dismissed. @@ -4216,7 +4214,7 @@ public class NotificationPanelViewController extends PanelViewController { } updateGestureExclusionRect(); mHeadsUpPinnedMode = inPinnedMode; - updateHeadsUpVisibility(); + updateVisibility(); mKeyguardStatusBarViewController.updateForHeadsUp(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java index e775e96de749..247ede91eeb3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java @@ -33,8 +33,6 @@ public abstract class PanelBar extends FrameLayout { private static final boolean SPEW = false; private static final String PANEL_BAR_SUPER_PARCELABLE = "panel_bar_super_parcelable"; private static final String STATE = "state"; - private boolean mBouncerShowing; - private boolean mExpanded; protected float mPanelFraction; public static final void LOG(String fmt, Object... args) { @@ -99,33 +97,6 @@ public abstract class PanelBar extends FrameLayout { pv.setBar(this); } - public void setBouncerShowing(boolean showing) { - mBouncerShowing = showing; - int important = showing ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - : IMPORTANT_FOR_ACCESSIBILITY_AUTO; - - setImportantForAccessibility(important); - updateVisibility(); - - if (mPanel != null) mPanel.getView().setImportantForAccessibility(important); - } - - public float getExpansionFraction() { - return mPanelFraction; - } - - public boolean isExpanded() { - return mExpanded; - } - - protected void updateVisibility() { - mPanel.getView().setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE); - } - - protected boolean shouldPanelBeVisible() { - return mExpanded || mBouncerShowing; - } - public boolean panelEnabled() { return true; } @@ -183,9 +154,7 @@ public abstract class PanelBar extends FrameLayout { boolean fullyClosed = true; boolean fullyOpened = false; if (SPEW) LOG("panelExpansionChanged: start state=%d, f=%.1f", mState, frac); - mExpanded = expanded; mPanelFraction = frac; - updateVisibility(); // adjust any other panels that may be partially visible if (expanded) { if (mState == STATE_CLOSED) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java index 51cae8c4af7f..b2155154d652 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.phone; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; + import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE; import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; import static com.android.systemui.classifier.Classifier.GENERIC; @@ -315,7 +318,7 @@ public abstract class PanelViewController { } private void startOpening(MotionEvent event) { - notifyBarPanelExpansionChanged(); + updatePanelExpansionAndVisibility(); maybeVibrateOnOpening(); //TODO: keyguard opens QS a different way; log that too? @@ -447,7 +450,7 @@ public abstract class PanelViewController { protected void onTrackingStopped(boolean expand) { mTracking = false; mBar.onTrackingStopped(expand); - notifyBarPanelExpansionChanged(); + updatePanelExpansionAndVisibility(); } protected void onTrackingStarted() { @@ -455,7 +458,7 @@ public abstract class PanelViewController { mTracking = true; mBar.onTrackingStarted(); notifyExpandingStarted(); - notifyBarPanelExpansionChanged(); + updatePanelExpansionAndVisibility(); } /** @@ -685,7 +688,7 @@ public abstract class PanelViewController { } else { cancelJankMonitoring(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); } - notifyBarPanelExpansionChanged(); + updatePanelExpansionAndVisibility(); } protected abstract boolean shouldUseDismissingAnimation(); @@ -760,7 +763,7 @@ public abstract class PanelViewController { mExpandedFraction = Math.min(1f, maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight); onHeightUpdated(mExpandedHeight); - notifyBarPanelExpansionChanged(); + updatePanelExpansionAndVisibility(); } /** @@ -878,7 +881,7 @@ public abstract class PanelViewController { if (mExpanding) { notifyExpandingFinished(); } - notifyBarPanelExpansionChanged(); + updatePanelExpansionAndVisibility(); // Wait for window manager to pickup the change, so we know the maximum height of the panel // then. @@ -916,7 +919,7 @@ public abstract class PanelViewController { } if (mInstantExpanding) { mInstantExpanding = false; - notifyBarPanelExpansionChanged(); + updatePanelExpansionAndVisibility(); } } @@ -1022,7 +1025,7 @@ public abstract class PanelViewController { public void onAnimationEnd(Animator animation) { setAnimator(null); onAnimationFinished.run(); - notifyBarPanelExpansionChanged(); + updatePanelExpansionAndVisibility(); } }); animator.start(); @@ -1059,19 +1062,39 @@ public abstract class PanelViewController { return animator; } - protected void notifyBarPanelExpansionChanged() { + /** Update the visibility of {@link PanelView} if necessary. */ + public void updateVisibility() { + mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE); + } + + /** Returns true if {@link PanelView} should be visible. */ + abstract boolean shouldPanelBeVisible(); + + /** + * Updates the panel expansion and {@link PanelView} visibility if necessary. + * + * TODO(b/200063118): Could public calls to this method be replaced with calls to + * {@link #updateVisibility()}? That would allow us to make this method private. + */ + public void updatePanelExpansionAndVisibility() { if (mBar != null) { - mBar.panelExpansionChanged( - mExpandedFraction, - mExpandedFraction > 0f || mInstantExpanding - || isPanelVisibleBecauseOfHeadsUp() || mTracking - || mHeightAnimator != null && !mIsSpringBackAnimation); + mBar.panelExpansionChanged(mExpandedFraction, isExpanded()); } + updateVisibility(); for (int i = 0; i < mExpansionListeners.size(); i++) { mExpansionListeners.get(i).onPanelExpansionChanged(mExpandedFraction, mTracking); } } + public boolean isExpanded() { + return mExpandedFraction > 0f + || mInstantExpanding + || isPanelVisibleBecauseOfHeadsUp() + || mTracking + || mHeightAnimator != null + && !mIsSpringBackAnimation; + } + public void addExpansionListener(PanelExpansionListener panelExpansionListener) { mExpansionListeners.add(panelExpansionListener); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index af556a26e3af..9ab6cdd3053b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -402,16 +402,6 @@ public class PhoneStatusBarView extends PanelBar { getPaddingBottom()); } - public void setHeadsUpVisible(boolean headsUpVisible) { - mHeadsUpVisible = headsUpVisible; - updateVisibility(); - } - - @Override - protected boolean shouldPanelBeVisible() { - return mHeadsUpVisible || super.shouldPanelBeVisible(); - } - /** An interface that will provide whether panel is enabled. */ interface PanelEnabledProvider { /** Returns true if the panel is enabled and false otherwise. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index 28040fd8d8a1..4c0332a75df1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -61,6 +61,10 @@ class PhoneStatusBarViewController( } } + fun setImportantForAccessibility(mode: Int) { + mView.importantForAccessibility = mode + } + private class StatusBarViewsCenterProvider : UnfoldMoveFromCenterAnimator.ViewCenterProvider { override fun getViewCenter(view: View, outPoint: Point) = when (view.id) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 4213902b7cf1..a564637aa510 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -1230,6 +1230,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump pw.println(mDefaultScrimAlpha); pw.print(" mExpansionFraction="); pw.println(mPanelExpansion); + pw.print(" mExpansionAffectsAlpha="); + pw.println(mExpansionAffectsAlpha); pw.print(" mState.getMaxLightRevealScrimAlpha="); pw.println(mState.getMaxLightRevealScrimAlpha()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index ee7725f670b0..3bdc08be63e6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -26,6 +26,8 @@ import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS; import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_STATUS_BARS; import static android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS; +import static androidx.core.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; +import static androidx.core.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; import static androidx.lifecycle.Lifecycle.State.RESUMED; import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME; @@ -135,6 +137,7 @@ import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.DelegateLaunchAnimatorController; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.battery.BatteryMeterViewController; import com.android.systemui.biometrics.AuthRippleController; @@ -669,7 +672,8 @@ public class StatusBar extends SystemUI implements private final SysuiStatusBarStateController mStatusBarStateController; private HeadsUpAppearanceController mHeadsUpAppearanceController; - private ActivityLaunchAnimator mActivityLaunchAnimator; + private final ActivityLaunchAnimator mActivityLaunchAnimator; + private final DialogLaunchAnimator mDialogLaunchAnimator; private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider; protected StatusBarNotificationPresenter mPresenter; private NotificationActivityStarter mNotificationActivityStarter; @@ -792,7 +796,9 @@ public class StatusBar extends SystemUI implements UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, Optional<StartingSurface> startingSurfaceOptional, TunerService tunerService, - DumpManager dumpManager) { + DumpManager dumpManager, + ActivityLaunchAnimator activityLaunchAnimator, + DialogLaunchAnimator dialogLaunchAnimator) { super(context); mNotificationsController = notificationsController; mLightBarController = lightBarController; @@ -900,6 +906,8 @@ public class StatusBar extends SystemUI implements }); mActivityIntentHelper = new ActivityIntentHelper(mContext); + mActivityLaunchAnimator = activityLaunchAnimator; + mDialogLaunchAnimator = dialogLaunchAnimator; // TODO(b/190746471): Find a better home for this. DateTimeView.setReceiverHandler(timeTickHandler); @@ -1139,6 +1147,8 @@ public class StatusBar extends SystemUI implements mNotificationPanelViewController.addExpansionListener( this::dispatchPanelExpansionForKeyguardDismiss); + mUserSwitcherController.init(mNotificationShadeWindowView); + // Allow plugins to reference DarkIconDispatcher and StatusBarStateController mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class); mPluginDependencyProvider.allowPluginDependency(StatusBarStateController.class); @@ -1180,22 +1190,13 @@ public class StatusBar extends SystemUI implements ); mBatteryMeterViewController.init(); - // CollapsedStatusBarFragment re-inflated PhoneStatusBarView and both of - // mStatusBarView.mExpanded and mStatusBarView.mBouncerShowing are false. - // PhoneStatusBarView's new instance will set to be gone in - // PanelBar.updateVisibility after calling mStatusBarView.setBouncerShowing - // that will trigger PanelBar.updateVisibility. If there is a heads up showing, - // it needs to notify PhoneStatusBarView's new instance to update the correct - // status by calling mNotificationPanel.notifyBarPanelExpansionChanged(). - if (mHeadsUpManager.hasPinnedHeadsUp()) { - mNotificationPanelViewController.notifyBarPanelExpansionChanged(); - } - mStatusBarView.setBouncerShowing(mBouncerShowing); - if (oldStatusBarView != null) { - float fraction = oldStatusBarView.getExpansionFraction(); - boolean expanded = oldStatusBarView.isExpanded(); - mStatusBarView.panelExpansionChanged(fraction, expanded); - } + // Ensure we re-propagate panel expansion values to the panel controller and + // any listeners it may have, such as PanelBar. This will also ensure we + // re-display the notification panel if necessary (for example, if + // a heads-up notification was being displayed and should continue being + // displayed). + mNotificationPanelViewController.updatePanelExpansionAndVisibility(); + setBouncerShowingForStatusBarComponents(mBouncerShowing); HeadsUpAppearanceController oldController = mHeadsUpAppearanceController; if (mHeadsUpAppearanceController != null) { @@ -1467,7 +1468,7 @@ public class StatusBar extends SystemUI implements private void setUpPresenter() { // Set up the initial notification state. - mActivityLaunchAnimator = new ActivityLaunchAnimator(mKeyguardHandler, mContext); + mActivityLaunchAnimator.setCallback(mKeyguardHandler); mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider( mNotificationShadeWindowViewController, mStackScrollerController.getNotificationListContainer(), @@ -2549,7 +2550,8 @@ public class StatusBar extends SystemUI implements animationController != null && !willLaunchResolverActivity && shouldAnimateLaunch( true /* isActivityIntent */); ActivityLaunchAnimator.Controller animController = - animate ? wrapAnimationController(animationController, dismissShade) : null; + animationController != null ? wrapAnimationController(animationController, + dismissShade) : null; // If we animate, we will dismiss the shade only once the animation is done. This is taken // care of by the StatusBarLaunchAnimationController. @@ -3522,7 +3524,7 @@ public class StatusBar extends SystemUI implements mBouncerShowing = bouncerShowing; mKeyguardBypassController.setBouncerShowing(bouncerShowing); mPulseExpansionHandler.setBouncerShowing(bouncerShowing); - if (mStatusBarView != null) mStatusBarView.setBouncerShowing(bouncerShowing); + setBouncerShowingForStatusBarComponents(bouncerShowing); updateHideIconsForBouncer(true /* animate */); mCommandQueue.recomputeDisableFlags(mDisplayId, true /* animate */); updateScrimController(); @@ -3532,6 +3534,23 @@ public class StatusBar extends SystemUI implements } /** + * Propagate the bouncer state to status bar components. + * + * Separate from {@link #setBouncerShowing} because we sometimes re-create the status bar and + * should update only the status bar components. + */ + private void setBouncerShowingForStatusBarComponents(boolean bouncerShowing) { + int importance = bouncerShowing + ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : IMPORTANT_FOR_ACCESSIBILITY_AUTO; + if (mPhoneStatusBarViewController != null) { + mPhoneStatusBarViewController.setImportantForAccessibility(importance); + } + mNotificationPanelViewController.setImportantForAccessibility(importance); + mNotificationPanelViewController.setBouncerShowing(bouncerShowing); + } + + /** * Collapses the notification shade if it is tracking or expanded. */ public void collapseShade() { @@ -3775,8 +3794,11 @@ public class StatusBar extends SystemUI implements || mKeyguardStateController.isKeyguardFadingAway(); // Do not animate the scrim expansion when triggered by the fingerprint sensor. - mScrimController.setExpansionAffectsAlpha( - !mBiometricUnlockController.isBiometricUnlock()); + boolean onKeyguardOrHidingIt = mKeyguardStateController.isShowing() + || mKeyguardStateController.isKeyguardFadingAway() + || mKeyguardStateController.isKeyguardGoingAway(); + mScrimController.setExpansionAffectsAlpha(!(mBiometricUnlockController.isBiometricUnlock() + && onKeyguardOrHidingIt)); boolean launchingAffordanceWithPreview = mNotificationPanelViewController.isLaunchingAffordanceWithPreview(); @@ -4200,10 +4222,9 @@ public class StatusBar extends SystemUI implements } private void sendInitialExpansionAmount(ExpansionChangedListener expansionChangedListener) { - if (mStatusBarView != null) { - expansionChangedListener.onExpansionChanged(mStatusBarView.getExpansionFraction(), - mStatusBarView.isExpanded()); - } + expansionChangedListener.onExpansionChanged( + mNotificationPanelViewController.getExpandedFraction(), + mNotificationPanelViewController.isExpanded()); } public void removeExpansionChangedListener(@NonNull ExpansionChangedListener listener) { @@ -4426,6 +4447,8 @@ public class StatusBar extends SystemUI implements && !mBiometricUnlockController.isWakeAndUnlock()) { mLightRevealScrim.setRevealAmount(1f - linear); } + + mDialogLaunchAnimator.onDozeAmountChanged(linear); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt index 515094bd6ec0..61552f065bc4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt @@ -70,6 +70,9 @@ class StatusBarContentInsetsProvider @Inject constructor( // (e.g. network displays) private val insetsCache = LruCache<CacheKey, Rect>(MAX_CACHE_SIZE) private val listeners = mutableSetOf<StatusBarContentInsetsChangedListener>() + private val isPrivacyDotEnabled: Boolean by lazy(LazyThreadSafetyMode.PUBLICATION) { + context.resources.getBoolean(R.bool.config_enablePrivacyDot) + } init { configurationController.addCallback(this) @@ -152,8 +155,9 @@ class StatusBarContentInsetsProvider @Inject constructor( val isRtl = rotatedResources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL val roundedCornerPadding = rotatedResources .getDimensionPixelSize(R.dimen.rounded_corner_content_padding) - val minDotWidth = rotatedResources - .getDimensionPixelSize(R.dimen.ongoing_appops_dot_min_padding) + val minDotWidth = if (isPrivacyDotEnabled) + rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_min_padding) + else 0 val minLeft: Int val minRight: Int diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt index 14e513a0556d..32aae6c05df6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt @@ -1,6 +1,7 @@ package com.android.systemui.statusbar.phone import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.LaunchAnimator /** * A [ActivityLaunchAnimator.Controller] that takes care of collapsing the status bar at the right @@ -22,7 +23,7 @@ class StatusBarLaunchAnimatorController( delegate.onLaunchAnimationStart(isExpandingFullyAbove) statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true) if (!isExpandingFullyAbove) { - statusBar.collapsePanelWithDuration(ActivityLaunchAnimator.ANIMATION_DURATION.toInt()) + statusBar.collapsePanelWithDuration(LaunchAnimator.ANIMATION_DURATION.toInt()) } } @@ -33,7 +34,7 @@ class StatusBarLaunchAnimatorController( } override fun onLaunchAnimationProgress( - state: ActivityLaunchAnimator.State, + state: LaunchAnimator.State, progress: Float, linearProgress: Float ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index 1e98c75f2616..9415d5082d10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -30,19 +30,24 @@ import android.view.WindowManager.LayoutParams; import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.animation.DialogListener; +import com.android.systemui.animation.ListenableDialog; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.LinkedHashSet; +import java.util.Set; + /** * Base class for dialogs that should appear over panels and keyguard. * The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast, * and dismisses itself when it receives the broadcast. */ -public class SystemUIDialog extends AlertDialog { - +public class SystemUIDialog extends AlertDialog implements ListenableDialog { private final Context mContext; private final DismissReceiver mDismissReceiver; + private final Set<DialogListener> mDialogListeners = new LinkedHashSet<>(); public SystemUIDialog(Context context) { this(context, R.style.Theme_SystemUI_Dialog); @@ -72,6 +77,43 @@ public class SystemUIDialog extends AlertDialog { mDismissReceiver.unregister(); } + @Override + public void addListener(DialogListener listener) { + mDialogListeners.add(listener); + } + + @Override + public void removeListener(DialogListener listener) { + mDialogListeners.remove(listener); + } + + @Override + public void dismiss() { + super.dismiss(); + + for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { + listener.onDismiss(); + } + } + + @Override + public void hide() { + super.hide(); + + for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { + listener.onHide(); + } + } + + @Override + public void show() { + super.show(); + + for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { + listener.onShow(); + } + } + public void setShowForAllUsers(boolean show) { setShowForAllUsers(this, show); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt new file mode 100644 index 000000000000..6a49a6da0d62 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIHostDialogProvider.kt @@ -0,0 +1,36 @@ +package com.android.systemui.statusbar.phone + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import com.android.systemui.animation.HostDialogProvider + +/** An implementation of [HostDialogProvider] to be used when animating SysUI dialogs. */ +class SystemUIHostDialogProvider : HostDialogProvider { + override fun createHostDialog( + context: Context, + theme: Int, + onCreateCallback: () -> Unit, + dismissOverride: (() -> Unit) -> Unit + ): Dialog { + return SystemUIHostDialog(context, theme, onCreateCallback, dismissOverride) + } + + private class SystemUIHostDialog( + context: Context, + theme: Int, + private val onCreateCallback: () -> Unit, + private val dismissOverride: (() -> Unit) -> Unit + ) : SystemUIDialog(context, theme) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + onCreateCallback() + } + + override fun dismiss() { + dismissOverride { + super.dismiss() + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java index 13eb75a4d508..0d43b9323a39 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java @@ -28,6 +28,8 @@ import com.android.internal.logging.MetricsLogger; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.InitController; +import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollector; @@ -235,7 +237,9 @@ public interface StatusBarPhoneModule { UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, Optional<StartingSurface> startingSurfaceOptional, TunerService tunerService, - DumpManager dumpManager) { + DumpManager dumpManager, + ActivityLaunchAnimator activityLaunchAnimator, + DialogLaunchAnimator dialogLaunchAnimator) { return new StatusBar( context, notificationsController, @@ -333,6 +337,8 @@ public interface StatusBarPhoneModule { unlockedScreenOffAnimationController, startingSurfaceOptional, tunerService, - dumpManager); + dumpManager, + activityLaunchAnimator, + dialogLaunchAnimator); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index 22f08ada5fc4..dadc01664b4d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -25,6 +25,7 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Dialog; +import android.app.IActivityManager; import android.app.IActivityTaskManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; @@ -55,6 +56,7 @@ import android.view.WindowManagerGlobal; import android.widget.BaseAdapter; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.RestrictedLockUtilsInternal; @@ -75,6 +77,7 @@ import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.tiles.UserDetailView; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.phone.NotificationShadeWindowView; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.user.CreateUserActivity; @@ -107,6 +110,7 @@ public class UserSwitcherController implements Dumpable { private static final int PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000; private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; + private static final long MULTI_USER_JOURNEY_TIMEOUT = 20000l; protected final Context mContext; protected final UserTracker mUserTracker; @@ -123,6 +127,7 @@ public class UserSwitcherController implements Dumpable { private final BroadcastDispatcher mBroadcastDispatcher; private final TelephonyListenerManager mTelephonyListenerManager; private final IActivityTaskManager mActivityTaskManager; + private final InteractionJankMonitor mInteractionJankMonitor; private ArrayList<UserRecord> mUsers = new ArrayList<>(); @VisibleForTesting @@ -141,15 +146,18 @@ public class UserSwitcherController implements Dumpable { private Intent mSecondaryUserServiceIntent; private SparseBooleanArray mForcePictureLoadForUserId = new SparseBooleanArray(2); private final UiEventLogger mUiEventLogger; + private final IActivityManager mActivityManager; public final DetailAdapter mUserDetailAdapter; private final Executor mBgExecutor; private final boolean mGuestUserAutoCreated; private final AtomicBoolean mGuestIsResetting; private final AtomicBoolean mGuestCreationScheduled; private FalsingManager mFalsingManager; + private NotificationShadeWindowView mRootView; @Inject public UserSwitcherController(Context context, + IActivityManager activityManager, UserManager userManager, UserTracker userTracker, KeyguardStateController keyguardStateController, @@ -165,14 +173,17 @@ public class UserSwitcherController implements Dumpable { UserDetailAdapter userDetailAdapter, SecureSettings secureSettings, @Background Executor bgExecutor, + InteractionJankMonitor interactionJankMonitor, DumpManager dumpManager) { mContext = context; + mActivityManager = activityManager; mUserTracker = userTracker; mBroadcastDispatcher = broadcastDispatcher; mTelephonyListenerManager = telephonyListenerManager; mActivityTaskManager = activityTaskManager; mUiEventLogger = uiEventLogger; mFalsingManager = falsingManager; + mInteractionJankMonitor = interactionJankMonitor; mGuestResumeSessionReceiver = new GuestResumeSessionReceiver( this, mUserTracker, mUiEventLogger, secureSettings); mUserDetailAdapter = userDetailAdapter; @@ -485,8 +496,11 @@ public class UserSwitcherController implements Dumpable { protected void switchToUserId(int id) { try { + mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder + .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mRootView) + .setTimeout(MULTI_USER_JOURNEY_TIMEOUT)); pauseRefreshUsers(); - ActivityManager.getService().switchUser(id); + mActivityManager.switchUser(id); } catch (RemoteException e) { Log.e(TAG, "Couldn't switch user.", e); } @@ -793,6 +807,10 @@ public class UserSwitcherController implements Dumpable { return guest.id; } + public void init(NotificationShadeWindowView notificationShadeWindowView) { + mRootView = notificationShadeWindowView; + } + public static abstract class BaseUserAdapter extends BaseAdapter { final UserSwitcherController mController; diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index d3557d4b1809..0772b2098565 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -78,6 +78,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.widget.ILockSettings; import com.android.internal.widget.LockPatternUtils; @@ -171,6 +172,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private FeatureFlags mFeatureFlags; @Mock + private InteractionJankMonitor mInteractionJankMonitor; + @Mock private Vibrator mVibrator; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateListenerCaptor; @@ -737,6 +740,16 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testMultiUserJankMonitor_whenUserSwitches() throws Exception { + final IRemoteCallback reply = new IRemoteCallback.Stub() { + @Override + public void sendResult(Bundle data) {} // do nothing + }; + mKeyguardUpdateMonitor.handleUserSwitchComplete(10 /* user */); + verify(mInteractionJankMonitor).end(eq(InteractionJankMonitor.CUJ_USER_SWITCH)); + } + + @Test public void testGetUserCanSkipBouncer_whenTrust() { int user = KeyguardUpdateMonitor.getCurrentUser(); mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */, user, 0 /* flags */); @@ -1051,7 +1064,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mRingerModeTracker, mBackgroundExecutor, mStatusBarStateController, mLockPatternUtils, mAuthController, mTelephonyListenerManager, mFeatureFlags, - mVibrator); + mInteractionJankMonitor, mVibrator); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 4c7f959e7b8e..c6df1c15e0b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -28,11 +28,13 @@ import static com.google.common.truth.Truth.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -151,27 +153,155 @@ public class ScreenDecorationsTest extends SysuiTestCase { reset(mTunerService); } + + private void verifyRoundedCornerViewsVisibility( + @DisplayCutout.BoundsPosition final int overlayId, + @View.Visibility final int visibility) { + final View overlay = mScreenDecorations.mOverlays[overlayId]; + final View left = overlay.findViewById(R.id.left); + final View right = overlay.findViewById(R.id.right); + assertNotNull(left); + assertNotNull(right); + assertThat(left.getVisibility()).isEqualTo(visibility); + assertThat(right.getVisibility()).isEqualTo(visibility); + } + + private void verifyTopDotViewsNullable(final boolean isAssertNull) { + if (isAssertNull) { + assertNull(mScreenDecorations.mTopLeftDot); + assertNull(mScreenDecorations.mTopRightDot); + } else { + assertNotNull(mScreenDecorations.mTopLeftDot); + assertNotNull(mScreenDecorations.mTopRightDot); + } + } + + private void verifyBottomDotViewsNullable(final boolean isAssertNull) { + if (isAssertNull) { + assertNull(mScreenDecorations.mBottomLeftDot); + assertNull(mScreenDecorations.mBottomRightDot); + } else { + assertNotNull(mScreenDecorations.mBottomLeftDot); + assertNotNull(mScreenDecorations.mBottomRightDot); + } + } + + private void verifyDotViewsNullable(final boolean isAssertNull) { + verifyTopDotViewsNullable(isAssertNull); + verifyBottomDotViewsNullable(isAssertNull); + } + + private void verifyTopDotViewsVisibility(@View.Visibility final int visibility) { + verifyTopDotViewsNullable(false); + assertThat(mScreenDecorations.mTopLeftDot.getVisibility()).isEqualTo(visibility); + assertThat(mScreenDecorations.mTopRightDot.getVisibility()).isEqualTo(visibility); + } + + private void verifyBottomDotViewsVisibility(@View.Visibility final int visibility) { + verifyBottomDotViewsNullable(false); + assertThat(mScreenDecorations.mBottomLeftDot.getVisibility()).isEqualTo(visibility); + assertThat(mScreenDecorations.mBottomRightDot.getVisibility()).isEqualTo(visibility); + } + + private void verifyDotViewsVisibility(@View.Visibility final int visibility) { + verifyTopDotViewsVisibility(visibility); + verifyBottomDotViewsVisibility(visibility); + } + + private void verifyOverlaysExistAndAdded(final boolean left, final boolean top, + final boolean right, final boolean bottom) { + if (left || top || right || bottom) { + assertNotNull(mScreenDecorations.mOverlays); + } else { + verify(mWindowManager, never()).addView(any(), any()); + return; + } + + if (left) { + assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]); + verify(mWindowManager, times(1)) + .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]), any()); + } else { + assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]); + } + + if (top) { + assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]); + verify(mWindowManager, times(1)) + .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any()); + } else { + assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]); + } + + if (right) { + assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); + verify(mWindowManager, times(1)) + .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]), any()); + } else { + assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); + } + + if (bottom) { + assertNotNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]); + verify(mWindowManager, times(1)) + .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]), any()); + } else { + assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]); + } + } + @Test - public void testNoRounding_NoCutout() { + public void testNoRounding_NoCutout_NoPrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 0 /* roundedPadding */, false /* multipleRadius */, - false /* fillCutout */); + false /* fillCutout */, false /* privacyDot */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); mScreenDecorations.start(); // No views added. - verify(mWindowManager, never()).addView(any(), any()); + verifyOverlaysExistAndAdded(false, false, false, false); // No Tuners tuned. verify(mTunerService, never()).addTunable(any(), any()); + // No dot controller init + verify(mDotViewController, never()).initialize(any(), any(), any(), any()); + } + + @Test + public void testNoRounding_NoCutout_PrivacyDot() { + setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + 0 /* roundedPadding */, false /* multipleRadius */, + false /* fillCutout */, true /* privacyDot */); + + // no cutout + doReturn(null).when(mScreenDecorations).getCutout(); + + mScreenDecorations.start(); + + // Top and bottom windows are created for privacy dot. + // Left and right window should be null. + verifyOverlaysExistAndAdded(false, true, false, true); + + // Rounded corner views shall not exist + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.GONE); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.GONE); + + // Privacy dots shall exist but invisible + verifyDotViewsVisibility(View.INVISIBLE); + + // One tunable. + verify(mTunerService, times(1)).addTunable(any(), any()); + // Dot controller init + verify(mDotViewController, times(1)).initialize( + isA(View.class), isA(View.class), isA(View.class), isA(View.class)); } @Test - public void testRounding_NoCutout() { + public void testRounding_NoCutout_NoPrivacyDot() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 20 /* roundedPadding */, false /* multipleRadius */, - false /* fillCutout */); + false /* fillCutout */, false /* privacyDot */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); @@ -179,17 +309,49 @@ public class ScreenDecorationsTest extends SysuiTestCase { mScreenDecorations.start(); // Top and bottom windows are created for rounded corners. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any()); - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]), any()); + // Left and right window should be null. + verifyOverlaysExistAndAdded(false, true, false, true); + + // Rounded corner views shall exist + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE); + + // Privacy dots shall not exist + verifyDotViewsNullable(true); + + // One tunable. + verify(mTunerService, times(1)).addTunable(any(), any()); + // No dot controller init + verify(mDotViewController, never()).initialize(any(), any(), any(), any()); + } + + @Test + public void testRounding_NoCutout_PrivacyDot() { + setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + 20 /* roundedPadding */, false /* multipleRadius */, + false /* fillCutout */, true /* privacyDot */); + + // no cutout + doReturn(null).when(mScreenDecorations).getCutout(); + + mScreenDecorations.start(); + // Top and bottom windows are created for rounded corners. // Left and right window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); + verifyOverlaysExistAndAdded(false, true, false, true); + + // Rounded corner views shall exist + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE); + + // Privacy dots shall exist but invisible + verifyDotViewsVisibility(View.INVISIBLE); // One tunable. verify(mTunerService, times(1)).addTunable(any(), any()); + // Dot controller init + verify(mDotViewController, times(1)).initialize( + isA(View.class), isA(View.class), isA(View.class), isA(View.class)); } @Test @@ -197,7 +359,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { final Point testRadiusPoint = new Point(1, 1); setupResources(1 /* radius */, 1 /* radiusTop */, 1 /* radiusBottom */, 0 /* roundedPadding */, false /* multipleRadius */, - false /* fillCutout */); + false /* fillCutout */, true /* privacyDot */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); @@ -213,7 +375,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { final int testTopRadius = 1; final int testBottomRadius = 5; setupResources(testTopRadius, testTopRadius, testBottomRadius, 0 /* roundedPadding */, - false /* multipleRadius */, false /* fillCutout */); + false /* multipleRadius */, false /* fillCutout */, true /* privacyDot */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); @@ -242,7 +404,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { final int testTopRadius = 1; final int testBottomRadius = 5; setupResources(testTopRadius, testTopRadius, testBottomRadius, 0 /* roundedPadding */, - false /* multipleRadius */, false /* fillCutout */); + false /* multipleRadius */, false /* fillCutout */, true /* privacyDot */); // left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null}; @@ -278,29 +440,67 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test - public void testRoundingMultipleRadius_NoCutout() { + public void testRoundingMultipleRadius_NoCutout_NoPrivacyDot() { final VectorDrawable d = (VectorDrawable) mContext.getDrawable(R.drawable.rounded); final Point multipleRadiusSize = new Point(d.getIntrinsicWidth(), d.getIntrinsicHeight()); setupResources(9999 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 9999 /* roundedPadding */, true /* multipleRadius */, - false /* fillCutout */); + false /* fillCutout */, false /* privacyDot */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); mScreenDecorations.start(); // Top and bottom windows are created for rounded corners. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any()); - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]), any()); + // Left and right window should be null. + verifyOverlaysExistAndAdded(false, true, false, true); + + // Rounded corner views shall exist + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE); + + // Privacy dots shall not exist + verifyDotViewsNullable(true); + // One tunable. + verify(mTunerService, times(1)).addTunable(any(), any()); + // No dot controller init + verify(mDotViewController, never()).initialize(any(), any(), any(), any()); + + // Size of corner view should exactly match max(width, height) of R.drawable.rounded + assertThat(mScreenDecorations.mRoundedDefault).isEqualTo(multipleRadiusSize); + assertThat(mScreenDecorations.mRoundedDefaultTop).isEqualTo(multipleRadiusSize); + assertThat(mScreenDecorations.mRoundedDefaultBottom).isEqualTo(multipleRadiusSize); + } + + @Test + public void testRoundingMultipleRadius_NoCutout_PrivacyDot() { + final VectorDrawable d = (VectorDrawable) mContext.getDrawable(R.drawable.rounded); + final Point multipleRadiusSize = new Point(d.getIntrinsicWidth(), d.getIntrinsicHeight()); + setupResources(9999 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + 9999 /* roundedPadding */, true /* multipleRadius */, + false /* fillCutout */, true /* privacyDot */); + + // no cutout + doReturn(null).when(mScreenDecorations).getCutout(); + + mScreenDecorations.start(); + // Top and bottom windows are created for rounded corners. // Left and right window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); + verifyOverlaysExistAndAdded(false, true, false, true); + + // Rounded corner views shall exist + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE); + + // Privacy dots shall exist but invisible + verifyDotViewsVisibility(View.INVISIBLE); // One tunable. verify(mTunerService, times(1)).addTunable(any(), any()); + // Dot controller init + verify(mDotViewController, times(1)).initialize( + isA(View.class), isA(View.class), isA(View.class), isA(View.class)); // Size of corner view should exactly match max(width, height) of R.drawable.rounded assertThat(mScreenDecorations.mRoundedDefault).isEqualTo(multipleRadiusSize); @@ -309,10 +509,10 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test - public void testNoRounding_CutoutShortEdge() { + public void testNoRounding_CutoutShortEdge_NoPrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 0 /* roundedPadding */, false /* multipleRadius */, - true /* fillCutout */); + true /* fillCutout */, false /* privacyDot */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -321,21 +521,53 @@ public class ScreenDecorationsTest extends SysuiTestCase { mScreenDecorations.start(); // Top window is created for top cutout. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any()); - // Bottom window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]); - // Left window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]); - // Right window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); + // Bottom, left, or right window should be null. + verifyOverlaysExistAndAdded(false, true, false, false); + + // Privacy dots shall not exist because of no privacy + verifyDotViewsNullable(true); + + // No dot controller init + verify(mDotViewController, never()).initialize(any(), any(), any(), any()); + } + + @Test + public void testNoRounding_CutoutShortEdge_PrivacyDot() { + setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + 0 /* roundedPadding */, false /* multipleRadius */, + true /* fillCutout */, true /* privacyDot */); + + // top cutout + final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; + doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds)) + .when(mScreenDecorations).getCutout(); + + mScreenDecorations.start(); + // Top window is created for top cutout. + // Bottom window is created for privacy dot. + // Left or right window should be null. + verifyOverlaysExistAndAdded(false, true, false, true); + + // Top rounded corner views shall exist because of cutout + // but be gone because of no rounded corner + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.GONE); + // Bottom rounded corner views shall exist because of privacy dot + // but be gone because of no rounded corner + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.GONE); + + // Privacy dots shall exist but invisible + verifyDotViewsVisibility(View.INVISIBLE); + + // Dot controller init + verify(mDotViewController, times(1)).initialize( + isA(View.class), isA(View.class), isA(View.class), isA(View.class)); } @Test - public void testNoRounding_CutoutLongEdge() { + public void testNoRounding_CutoutLongEdge_NoPrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 0 /* roundedPadding */, false /* multipleRadius */, - true /* fillCutout */); + true /* fillCutout */, false /* privacyDot */); // left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null}; @@ -344,21 +576,50 @@ public class ScreenDecorationsTest extends SysuiTestCase { mScreenDecorations.start(); // Left window is created for left cutout. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]), any()); - // Bottom window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]); - // Top window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]); - // Right window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); + // Bottom, top, or right window should be null. + verifyOverlaysExistAndAdded(true, false, false, false); + + // Left rounded corner views shall exist because of cutout + // but be gone because of no rounded corner + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_LEFT, View.GONE); + + // Top privacy dots shall not exist because of no privacy + verifyDotViewsNullable(true); + + // No dot controller init + verify(mDotViewController, never()).initialize(any(), any(), any(), any()); } @Test - public void testRounding_CutoutShortEdge() { + public void testNoRounding_CutoutLongEdge_PrivacyDot() { + setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + 0 /* roundedPadding */, false /* multipleRadius */, + true /* fillCutout */, true /* privacyDot */); + + // left cutout + final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null}; + doReturn(getDisplayCutoutForRotation(Insets.of(1, 0, 0, 0), bounds)) + .when(mScreenDecorations).getCutout(); + + mScreenDecorations.start(); + // Left window is created for left cutout. + // Right window is created for privacy. + // Bottom, or top window should be null. + verifyOverlaysExistAndAdded(true, false, true, false); + + // Privacy dots shall exist but invisible + verifyDotViewsVisibility(View.INVISIBLE); + + // Dot controller init + verify(mDotViewController, times(1)).initialize( + isA(View.class), isA(View.class), isA(View.class), isA(View.class)); + } + + @Test + public void testRounding_CutoutShortEdge_NoPrivacyDot() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 20 /* roundedPadding */, false /* multipleRadius */, - true /* fillCutout */); + true /* fillCutout */, false /* privacyDot */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -367,22 +628,55 @@ public class ScreenDecorationsTest extends SysuiTestCase { mScreenDecorations.start(); // Top window is created for rounded corner and top cutout. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any()); // Bottom window is created for rounded corner. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]), any()); - // Left window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]); - // Right window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); + // Left, or right window should be null. + verifyOverlaysExistAndAdded(false, true, false, true); + + // Rounded corner views shall exist + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE); + + // Top privacy dots shall not exist because of no privacy dot + verifyDotViewsNullable(true); + + // No dot controller init + verify(mDotViewController, never()).initialize(any(), any(), any(), any()); } @Test - public void testRounding_CutoutLongEdge() { + public void testRounding_CutoutShortEdge_PrivacyDot() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 20 /* roundedPadding */, false /* multipleRadius */, - true /* fillCutout */); + true /* fillCutout */, true /* privacyDot */); + + // top cutout + final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; + doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds)) + .when(mScreenDecorations).getCutout(); + + mScreenDecorations.start(); + // Top window is created for rounded corner and top cutout. + // Bottom window is created for rounded corner. + // Left, or right window should be null. + verifyOverlaysExistAndAdded(false, true, false, true); + + // Rounded corner views shall exist + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.VISIBLE); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.VISIBLE); + + // Top privacy dots shall exist but invisible + verifyDotViewsVisibility(View.INVISIBLE); + + // Dot controller init + verify(mDotViewController, times(1)).initialize( + isA(View.class), isA(View.class), isA(View.class), isA(View.class)); + } + + @Test + public void testRounding_CutoutLongEdge_NoPrivacyDot() { + setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + 20 /* roundedPadding */, false /* multipleRadius */, + true /* fillCutout */, false /* privacyDot */); // left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null}; @@ -391,22 +685,53 @@ public class ScreenDecorationsTest extends SysuiTestCase { mScreenDecorations.start(); // Left window is created for rounded corner and left cutout. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]), any()); // Right window is created for rounded corner. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]), any()); - // Top window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]); - // Bottom window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]); + // Top, or bottom window should be null. + verifyOverlaysExistAndAdded(true, false, true, false); + } + + @Test + public void testRounding_CutoutLongEdge_PrivacyDot() { + setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + 20 /* roundedPadding */, false /* multipleRadius */, + true /* fillCutout */, true /* privacyDot */); + + // left cutout + final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null}; + doReturn(getDisplayCutoutForRotation(Insets.of(1, 0, 0, 0), bounds)) + .when(mScreenDecorations).getCutout(); + + mScreenDecorations.start(); + // Left window is created for rounded corner, left cutout, and privacy. + // Right window is created for rounded corner and privacy dot. + // Top, or bottom window should be null. + verifyOverlaysExistAndAdded(true, false, true, false); + } + + @Test + public void testRounding_CutoutShortAndLongEdge_NoPrivacyDot() { + setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + 20 /* roundedPadding */, false /* multipleRadius */, + true /* fillCutout */, false /* privacyDot */); + + // top and left cutout + final Rect[] bounds = {new Rect(0, 50, 1, 60), new Rect(9, 0, 10, 1), null, null}; + doReturn(getDisplayCutoutForRotation(Insets.of(1, 1, 0, 0), bounds)) + .when(mScreenDecorations).getCutout(); + + mScreenDecorations.start(); + // Top window is created for rounded corner and top cutout. + // Bottom window is created for rounded corner. + // Left window is created for left cutout. + // Right window should be null. + verifyOverlaysExistAndAdded(true, true, false, true); } @Test - public void testRounding_CutoutShortAndLongEdge() { + public void testRounding_CutoutShortAndLongEdge_PrivacyDot() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 20 /* roundedPadding */, false /* multipleRadius */, - true /* fillCutout */); + true /* fillCutout */, true /* privacyDot */); // top and left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), new Rect(9, 0, 10, 1), null, null}; @@ -415,23 +740,17 @@ public class ScreenDecorationsTest extends SysuiTestCase { mScreenDecorations.start(); // Top window is created for rounded corner and top cutout. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any()); // Bottom window is created for rounded corner. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]), any()); // Left window is created for left cutout. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]), any()); // Right window should be null. - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); + verifyOverlaysExistAndAdded(true, true, false, true); } @Test - public void testNoRounding_SwitchFrom_ShortEdgeCutout_To_LongCutout() { + public void testNoRounding_SwitchFrom_ShortEdgeCutout_To_LongCutout_NoPrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 0 /* roundedPadding */, false /* multipleRadius */, - true /* fillCutout */); + true /* fillCutout */, false /* privacyDot */); // Set to short edge cutout(top). final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -439,11 +758,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { .when(mScreenDecorations).getCutout(); mScreenDecorations.start(); - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any()); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]); + verifyOverlaysExistAndAdded(false, true, false, false); // Switch to long edge cutout(left). final Rect[] newBounds = {new Rect(0, 50, 1, 60), null, null, null}; @@ -451,18 +766,37 @@ public class ScreenDecorationsTest extends SysuiTestCase { .when(mScreenDecorations).getCutout(); mScreenDecorations.onConfigurationChanged(new Configuration()); - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]), any()); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]); + verifyOverlaysExistAndAdded(true, false, false, false); } @Test - public void testDelayedCutout() { + public void testNoRounding_SwitchFrom_ShortEdgeCutout_To_LongCutout_PrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 0 /* roundedPadding */, false /* multipleRadius */, - false /* fillCutout */); + true /* fillCutout */, true /* privacyDot */); + + // Set to short edge cutout(top). + final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; + doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds)) + .when(mScreenDecorations).getCutout(); + + mScreenDecorations.start(); + verifyOverlaysExistAndAdded(false, true, false, true); + + // Switch to long edge cutout(left). + final Rect[] newBounds = {new Rect(0, 50, 1, 60), null, null, null}; + doReturn(getDisplayCutoutForRotation(Insets.of(1, 0, 0, 0), newBounds)) + .when(mScreenDecorations).getCutout(); + + mScreenDecorations.onConfigurationChanged(new Configuration()); + verifyOverlaysExistAndAdded(true, false, true, false); + } + + @Test + public void testDelayedCutout_NoPrivacyDot() { + setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + 0 /* roundedPadding */, false /* multipleRadius */, + false /* fillCutout */, false /* privacyDot */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -478,11 +812,38 @@ public class ScreenDecorationsTest extends SysuiTestCase { mScreenDecorations.onConfigurationChanged(new Configuration()); // Only top windows should be added. - verify(mWindowManager, times(1)) - .addView(eq(mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP]), any()); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM]); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_LEFT]); - assertNull(mScreenDecorations.mOverlays[BOUNDS_POSITION_RIGHT]); + verifyOverlaysExistAndAdded(false, true, false, false); + } + + @Test + public void testDelayedCutout_PrivacyDot() { + setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + 0 /* roundedPadding */, false /* multipleRadius */, + false /* fillCutout */, true /* privacyDot */); + + // top cutout + final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; + doReturn(getDisplayCutoutForRotation(Insets.of(0, 1, 0, 0), bounds)) + .when(mScreenDecorations).getCutout(); + + mScreenDecorations.start(); + // Both top and bottom windows should be added because of privacy dot, + // but their visibility shall be gone because of no rounding. + verifyOverlaysExistAndAdded(false, true, false, true); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.GONE); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.GONE); + + when(mContext.getResources().getBoolean( + com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout)) + .thenReturn(true); + mScreenDecorations.onConfigurationChanged(new Configuration()); + + assertNotNull(mScreenDecorations.mOverlays); + // Both top and bottom windows should be added because of privacy dot, + // but their visibility shall be gone because of no rounding. + verifyOverlaysExistAndAdded(false, true, false, true); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_TOP, View.GONE); + verifyRoundedCornerViewsVisibility(BOUNDS_POSITION_BOTTOM, View.GONE); } @Test @@ -496,7 +857,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testUpdateRoundedCorners() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, 0 /* roundedPadding */, false /* multipleRadius */, - false /* fillCutout */); + false /* fillCutout */, true /* privacyDot */); mScreenDecorations.start(); assertEquals(mScreenDecorations.mRoundedDefault, new Point(20, 20)); @@ -511,7 +872,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testOnlyRoundedCornerRadiusTop() { setupResources(0 /* radius */, 10 /* radiusTop */, 0 /* radiusBottom */, 0 /* roundedPadding */, false /* multipleRadius */, - false /* fillCutout */); + false /* fillCutout */, true /* privacyDot */); mScreenDecorations.start(); assertEquals(new Point(0, 0), mScreenDecorations.mRoundedDefault); @@ -523,7 +884,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testOnlyRoundedCornerRadiusBottom() { setupResources(0 /* radius */, 0 /* radiusTop */, 20 /* radiusBottom */, 0 /* roundedPadding */, false /* multipleRadius */, - false /* fillCutout */); + false /* fillCutout */, true /* privacyDot */); mScreenDecorations.start(); assertEquals(new Point(0, 0), mScreenDecorations.mRoundedDefault); @@ -531,7 +892,6 @@ public class ScreenDecorationsTest extends SysuiTestCase { assertEquals(new Point(20, 20), mScreenDecorations.mRoundedDefaultBottom); } - @Test public void testBoundingRectsToRegion() throws Exception { Rect rect = new Rect(1, 2, 3, 4); @@ -588,7 +948,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { } private void setupResources(int radius, int radiusTop, int radiusBottom, int roundedPadding, - boolean multipleRadius, boolean fillCutout) { + boolean multipleRadius, boolean fillCutout, boolean privacyDot) { mContext.getOrCreateTestableResources().addOverride( com.android.internal.R.array.config_displayUniqueIdArray, new String[]{}); @@ -625,6 +985,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { R.bool.config_roundedCornerMultipleRadius, multipleRadius); mContext.getOrCreateTestableResources().addOverride( com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout, fillCutout); + mContext.getOrCreateTestableResources().addOverride( + R.bool.config_enablePrivacyDot, privacyDot); } private DisplayCutout getDisplayCutoutForRotation(Insets safeInsets, Rect[] cutoutBounds) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt index cc35a8f9e1b5..d819fa2adc38 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt @@ -46,6 +46,7 @@ import org.mockito.junit.MockitoJUnit @RunWithLooper class ActivityLaunchAnimatorTest : SysuiTestCase() { private val launchContainer = LinearLayout(mContext) + private val launchAnimator = LaunchAnimator(mContext, isForTesting = true) @Mock lateinit var callback: ActivityLaunchAnimator.Callback @Spy private val controller = TestLaunchAnimatorController(launchContainer) @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback @@ -56,7 +57,8 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { @Before fun setup() { - activityLaunchAnimator = ActivityLaunchAnimator(callback, mContext) + activityLaunchAnimator = ActivityLaunchAnimator(launchAnimator) + activityLaunchAnimator.callback = callback } private fun startIntentWithAnimation( @@ -120,7 +122,8 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { @Test fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() { `when`(callback.isOnKeyguard()).thenReturn(true) - val animator = ActivityLaunchAnimator(callback, context) + val animator = ActivityLaunchAnimator(launchAnimator) + animator.callback = callback val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) var animationAdapter: RemoteAnimationAdapter? = null @@ -208,7 +211,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { private class TestLaunchAnimatorController( override var launchContainer: ViewGroup ) : ActivityLaunchAnimator.Controller { - override fun createAnimatorState() = ActivityLaunchAnimator.State( + override fun createAnimatorState() = LaunchAnimator.State( top = 100, bottom = 200, left = 300, @@ -232,7 +235,7 @@ private class TestLaunchAnimatorController( } override fun onLaunchAnimationProgress( - state: ActivityLaunchAnimator.State, + state: LaunchAnimator.State, progress: Float, linearProgress: Float ) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt new file mode 100644 index 000000000000..5bcf828afe9f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt @@ -0,0 +1,186 @@ +package com.android.systemui.animation + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.ViewUtils +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.LinearLayout +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class DialogLaunchAnimatorTest : SysuiTestCase() { + private val launchAnimator = LaunchAnimator(context, isForTesting = true) + private val hostDialogprovider = TestHostDialogProvider() + private val dialogLaunchAnimator = + DialogLaunchAnimator(context, launchAnimator, hostDialogprovider) + + @Test + fun testShowDialogFromView() { + // Show the dialog. showFromView() must be called on the main thread with a dialog created + // on the main thread too. + val (dialog, hostDialog) = runOnMainThreadAndWaitForIdleSync { + val touchSurfaceRoot = LinearLayout(context) + val touchSurface = View(context) + touchSurfaceRoot.addView(touchSurface) + + // We need to attach the root to the window manager otherwise the exit animation will + // be skipped + ViewUtils.attachView(touchSurfaceRoot) + + val dialog = TestDialog(context) + val hostDialog = + dialogLaunchAnimator.showFromView(dialog, touchSurface) as TestHostDialog + dialog to hostDialog + } + + // Only the host dialog is actually showing. + assertTrue(hostDialog.isShowing) + assertFalse(dialog.isShowing) + + // The dialog onStart() method was called but not onStop(). + assertTrue(dialog.onStartCalled) + assertFalse(dialog.onStopCalled) + + // The dialog content has been stolen and is shown inside the host dialog. + val hostDialogContent = hostDialog.findViewById<ViewGroup>(android.R.id.content) + assertEquals(0, dialog.findViewById<ViewGroup>(android.R.id.content).childCount) + assertEquals(1, hostDialogContent.childCount) + + val hostDialogRoot = hostDialogContent.getChildAt(0) as ViewGroup + assertEquals(1, hostDialogRoot.childCount) + assertEquals(dialog.contentView, hostDialogRoot.getChildAt(0)) + + // If we are dozing, the host dialog window also fades out. + runOnMainThreadAndWaitForIdleSync { dialogLaunchAnimator.onDozeAmountChanged(0.5f) } + assertTrue(hostDialog.window!!.decorView.alpha < 1f) + + // Hiding/showing/dismissing the dialog should hide/show/dismiss the host dialog given that + // it's a ListenableDialog. + runOnMainThreadAndWaitForIdleSync { dialog.hide() } + assertFalse(hostDialog.isShowing) + assertFalse(dialog.isShowing) + + runOnMainThreadAndWaitForIdleSync { dialog.show() } + assertTrue(hostDialog.isShowing) + assertFalse(dialog.isShowing) + + assertFalse(dialog.onStopCalled) + runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } + assertFalse(hostDialog.isShowing) + assertFalse(dialog.isShowing) + assertTrue(hostDialog.wasDismissed) + assertTrue(dialog.onStopCalled) + } + + private fun <T : Any> runOnMainThreadAndWaitForIdleSync(f: () -> T): T { + lateinit var result: T + context.mainExecutor.execute { + result = f() + } + waitForIdleSync() + return result + } + + private class TestHostDialogProvider : HostDialogProvider { + override fun createHostDialog( + context: Context, + theme: Int, + onCreateCallback: () -> Unit, + dismissOverride: (() -> Unit) -> Unit + ): Dialog = TestHostDialog(context, onCreateCallback, dismissOverride) + } + + private class TestHostDialog( + context: Context, + private val onCreateCallback: () -> Unit, + private val dismissOverride: (() -> Unit) -> Unit + ) : Dialog(context) { + var wasDismissed = false + + init { + // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw. + window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + onCreateCallback() + } + + override fun dismiss() { + dismissOverride { + super.dismiss() + wasDismissed = true + } + } + } + + private class TestDialog(context: Context) : Dialog(context), ListenableDialog { + private val listeners = hashSetOf<DialogListener>() + val contentView = View(context) + var onStartCalled = false + var onStopCalled = false + + init { + // We need to set the window type for dialogs shown by SysUI, otherwise WM will throw. + window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(contentView) + } + + override fun onStart() { + super.onStart() + onStartCalled = true + } + + override fun onStop() { + super.onStart() + onStopCalled = true + } + + override fun addListener(listener: DialogListener) { + listeners.add(listener) + } + + override fun removeListener(listener: DialogListener) { + listeners.remove(listener) + } + + override fun dismiss() { + super.dismiss() + notifyListeners { onDismiss() } + } + + override fun hide() { + super.hide() + notifyListeners { onHide() } + } + + override fun show() { + super.show() + notifyListeners { onShow() } + } + + private fun notifyListeners(notify: DialogListener.() -> Unit) { + for (listener in HashSet(listeners)) { + listener.notify() + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt index 8cba25dc1b92..58e0cb259bb2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt @@ -32,7 +32,7 @@ class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() { fun animatingOrphanViewDoesNotCrash() { val ghostedView = LinearLayout(mContext) val controller = GhostedViewLaunchAnimatorController(ghostedView) - val state = ActivityLaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0) + val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0) controller.onIntentStarted(willAnimate = true) controller.onLaunchAnimationStart(isExpandingFullyAbove = true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt index 47c5545ab587..d864aaeadee3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -405,6 +405,26 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() { + val recommendationExtras = Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", null) + } + whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras) + whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction) + whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf()) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + + verify(listener).onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq(EMPTY_SMARTSPACE_MEDIA_DATA + .copy(targetId = KEY_MEDIA_SMARTSPACE, isActive = true, + isValid = false, dismissIntent = null)), + eq(false)) + } + + @Test fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() { smartspaceMediaDataProvider.onTargetsAvailable(listOf()) verify(listener, never()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index 2c686618a361..25ca8c95500b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -54,6 +54,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { // Mock private MediaOutputController mMediaOutputController = mock(MediaOutputController.class); + private MediaOutputDialog mMediaOutputDialog = mock(MediaOutputDialog.class); private MediaDevice mMediaDevice1 = mock(MediaDevice.class); private MediaDevice mMediaDevice2 = mock(MediaDevice.class); private Icon mIcon = mock(Icon.class); @@ -65,7 +66,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Before public void setUp() { - mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController, mMediaOutputDialog); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index 9bd07b88417d..053851ec385d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -39,6 +39,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.phone.ShadeController; @@ -63,6 +64,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { private NotificationEntryManager mNotificationEntryManager = mock(NotificationEntryManager.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); + private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl; private MediaOutputController mMediaOutputController; @@ -75,7 +77,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { public void setUp() { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mMediaOutputController); mMediaOutputBaseDialogImpl.onCreate(new Bundle()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index d1a617bcc0cb..f7e60caa2624 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -49,6 +49,7 @@ import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -91,6 +92,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { private NotificationEntryManager mNotificationEntryManager = mock(NotificationEntryManager.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); + private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); private Context mSpyContext; private MediaOutputController mMediaOutputController; @@ -113,7 +115,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; MediaDescription.Builder builder = new MediaDescription.Builder(); @@ -157,7 +159,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void start_withoutPackageName_verifyMediaControllerInit() { mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); mMediaOutputController.start(mCb); @@ -178,7 +180,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void stop_withoutPackageName_verifyMediaControllerDeinit() { mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); mMediaOutputController.start(mCb); @@ -449,7 +451,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void getNotificationLargeIcon_withoutPackageName_returnsNull() { mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); assertThat(mMediaOutputController.getNotificationIcon()).isNull(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java index 86f6bdec43f0..8a3ea562269d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java @@ -36,6 +36,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.phone.ShadeController; @@ -65,6 +66,7 @@ public class MediaOutputDialogTest extends SysuiTestCase { private final NotificationEntryManager mNotificationEntryManager = mock(NotificationEntryManager.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); + private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); private MediaOutputDialog mMediaOutputDialog; private MediaOutputController mMediaOutputController; @@ -74,10 +76,11 @@ public class MediaOutputDialogTest extends SysuiTestCase { public void setUp() { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputDialog = new MediaOutputDialog(mContext, false, mMediaOutputController, mUiEventLogger); + mMediaOutputDialog.show(); when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice); when(mMediaDevice.getFeatures()).thenReturn(mFeatures); @@ -123,6 +126,7 @@ public class MediaOutputDialogTest extends SysuiTestCase { public void onCreate_ShouldLogVisibility() { MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false, mMediaOutputController, mUiEventLogger); + testDialog.show(); testDialog.dismissDialog(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java index c296ff5cf19a..e8cd6c88956d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java @@ -34,6 +34,7 @@ import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.phone.ShadeController; @@ -64,6 +65,7 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase { private NotificationEntryManager mNotificationEntryManager = mock(NotificationEntryManager.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); + private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); private MediaOutputGroupDialog mMediaOutputGroupDialog; private MediaOutputController mMediaOutputController; @@ -73,10 +75,11 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase { public void setUp() { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false, mMediaOutputController); + mMediaOutputGroupDialog.show(); when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt index fd932803ff37..ee9c2b82c283 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt @@ -199,8 +199,9 @@ class LockscreenSmartspaceControllerTest : SysuiTestCase() { `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(false) `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(false) - // WHEN a connection attempt is made - controller.buildAndConnectView(fakeParent) + // WHEN a connection attempt is made and view is attached + val view = controller.buildAndConnectView(fakeParent) + controller.stateChangeListener.onViewAttachedToWindow(view) // THEN no session is created verify(smartspaceManager, never()).createSmartspaceSession(any()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java index 1f3e1c7a6b86..902d11575597 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java @@ -32,6 +32,7 @@ import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; @@ -81,6 +82,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationRankin import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder; import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -94,6 +96,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -120,6 +123,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { @Mock private KeyguardEnvironment mEnvironment; @Mock private ExpandableNotificationRow mRow; @Mock private NotificationEntryListener mEntryListener; + @Mock private NotifCollectionListener mNotifCollectionListener; @Mock private NotificationRemoveInterceptor mRemoveInterceptor; @Mock private HeadsUpManager mHeadsUpManager; @Mock private RankingMap mRankingMap; @@ -215,6 +219,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { mEnvironment)); mEntryManager.setUpWithPresenter(mPresenter); mEntryManager.addNotificationEntryListener(mEntryListener); + mEntryManager.addCollectionListener(mNotifCollectionListener); mEntryManager.addNotificationRemoveInterceptor(mRemoveInterceptor); setUserSentiment(mSbn.getKey(), Ranking.USER_SENTIMENT_NEUTRAL); @@ -318,13 +323,20 @@ public class NotificationEntryManagerTest extends SysuiTestCase { eq(mEntry), any(), eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON)); } + /** Regression test for b/201097913. */ @Test - public void testRemoveNotification_whilePending() { + public void testRemoveNotification_whilePending_onlyCollectionListenerNotified() { + // Add and then remove a pending entry (entry that hasn't been inflated). mEntryManager.addNotification(mSbn, mRankingMap); mEntryManager.removeNotification(mSbn.getKey(), mRankingMap, UNDEFINED_DISMISS_REASON); + // Verify that only the listener for the NEW pipeline is notified. + // Old pipeline: verify(mEntryListener, never()).onEntryRemoved( - eq(mEntry), any(), eq(false /* removedByUser */), eq(UNDEFINED_DISMISS_REASON)); + argThat(matchEntryOnSbn()), any(), anyBoolean(), anyInt()); + // New pipeline: + verify(mNotifCollectionListener).onEntryRemoved( + argThat(matchEntryOnSbn()), anyInt()); } @Test @@ -639,6 +651,11 @@ public class NotificationEntryManagerTest extends SysuiTestCase { PendingIntent.FLAG_IMMUTABLE)).build(); } + // TODO(b/201321631): Update more tests to use this function instead of eq(mEntry). + private ArgumentMatcher<NotificationEntry> matchEntryOnSbn() { + return e -> e.getSbn().equals(mSbn); + } + private static class FakeNotificationLifetimeExtender implements NotificationLifetimeExtender { private NotificationSafeToRemoveCallback mCallback; private boolean mExtendLifetimes = true; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java index 33928ea78368..f7423bb7951d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java @@ -156,8 +156,6 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { private KeyguardBottomAreaView mQsFrame; private KeyguardStatusView mKeyguardStatusView; @Mock - private ViewGroup mBigClockContainer; - @Mock private NotificationIconAreaController mNotificationAreaController; @Mock private HeadsUpManagerPhone mHeadsUpManager; @@ -347,7 +345,6 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mKeyguardBottomArea.getLeftView()).thenReturn(mock(KeyguardAffordanceView.class)); when(mKeyguardBottomArea.getRightView()).thenReturn(mock(KeyguardAffordanceView.class)); when(mKeyguardBottomArea.animate()).thenReturn(mock(ViewPropertyAnimator.class)); - when(mView.findViewById(R.id.big_clock_container)).thenReturn(mBigClockContainer); when(mView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame); when(mView.findViewById(R.id.keyguard_status_view)) .thenReturn(mock(KeyguardStatusView.class)); @@ -778,6 +775,18 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { } @Test + public void testSwitchesToBigClockInSplitShadeOnAod() { + mStatusBarStateController.setState(KEYGUARD); + enableSplitShade(/* enabled= */ true); + when(mMediaDataManager.hasActiveMedia()).thenReturn(true); + when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); + + mNotificationPanelViewController.setDozing(true, false, null); + + verify(mKeyguardStatusViewController).displayClock(LARGE); + } + + @Test public void testDisplaysSmallClockOnLockscreenInSplitShadeWhenMediaIsPlaying() { mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index 88a3827d0582..fdda76df64d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -77,6 +77,8 @@ import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.InitController; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollectorFake; @@ -274,6 +276,8 @@ public class StatusBarTest extends SysuiTestCase { @Mock private StartingSurface mStartingSurface; @Mock private OperatorNameViewController mOperatorNameViewController; @Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory; + @Mock private ActivityLaunchAnimator mActivityLaunchAnimator; + @Mock private DialogLaunchAnimator mDialogLaunchAnimator; private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); @@ -452,7 +456,9 @@ public class StatusBarTest extends SysuiTestCase { mUnlockedScreenOffAnimationController, Optional.of(mStartingSurface), mTunerService, - mock(DumpManager.class)); + mock(DumpManager.class), + mActivityLaunchAnimator, + mDialogLaunchAnimator); when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class), any(NotificationPanelViewController.class), any(BiometricUnlockController.class), any(ViewGroup.class), any(KeyguardBypassController.class))) @@ -794,6 +800,34 @@ public class StatusBarTest extends SysuiTestCase { } @Test + public void testSetExpansionAffectsAlpha_onlyWhenHidingKeyguard() { + mStatusBar.updateScrimController(); + verify(mScrimController).setExpansionAffectsAlpha(eq(true)); + + clearInvocations(mScrimController); + when(mBiometricUnlockController.isBiometricUnlock()).thenReturn(true); + mStatusBar.updateScrimController(); + verify(mScrimController).setExpansionAffectsAlpha(eq(true)); + + clearInvocations(mScrimController); + when(mKeyguardStateController.isShowing()).thenReturn(true); + mStatusBar.updateScrimController(); + verify(mScrimController).setExpansionAffectsAlpha(eq(false)); + + clearInvocations(mScrimController); + reset(mKeyguardStateController); + when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true); + mStatusBar.updateScrimController(); + verify(mScrimController).setExpansionAffectsAlpha(eq(false)); + + clearInvocations(mScrimController); + reset(mKeyguardStateController); + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); + mStatusBar.updateScrimController(); + verify(mScrimController).setExpansionAffectsAlpha(eq(false)); + } + + @Test public void testTransitionLaunch_noPreview_doesntGoUnlocked() { mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD); mStatusBar.showKeyguardImpl(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt index dba83e1eeeb9..dd43ea56609b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.policy +import android.app.IActivityManager import android.app.IActivityTaskManager import android.app.admin.DevicePolicyManager import android.content.Context @@ -31,6 +32,7 @@ import android.os.UserManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest +import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.util.UserIcons import com.android.systemui.GuestResumeSessionReceiver @@ -54,10 +56,11 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.Mockito.`when` import org.mockito.Mockito.any import org.mockito.Mockito.anyString import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -65,6 +68,7 @@ import org.mockito.MockitoAnnotations @SmallTest class UserSwitcherControllerTest : SysuiTestCase() { @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var activityManager: IActivityManager @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var handler: Handler @@ -78,6 +82,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { @Mock private lateinit var secureSettings: SecureSettings @Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var dumpManager: DumpManager + @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor private lateinit var testableLooper: TestableLooper private lateinit var uiBgExecutor: FakeExecutor private lateinit var uiEventLogger: UiEventLoggerFake @@ -110,6 +115,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { userSwitcherController = UserSwitcherController( context, + activityManager, userManager, userTracker, keyguardStateController, @@ -125,6 +131,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { userDetailAdapter, secureSettings, uiBgExecutor, + interactionJankMonitor, dumpManager) userSwitcherController.mPauseRefreshUsers = true @@ -132,7 +139,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { } @Test - fun testAddGuest_okButtonPressed_isLogged() { + fun testAddGuest_okButtonPressed() { val emptyGuestUserRecord = UserSwitcherController.UserRecord( null, null, @@ -148,6 +155,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { userSwitcherController.onUserListItemClicked(emptyGuestUserRecord) testableLooper.processAllMessages() + verify(interactionJankMonitor).begin(any()) + verify(activityManager).switchUser(guestInfo.id) assertEquals(1, uiEventLogger.numLogs()) assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_ADD.id, uiEventLogger.eventId(0)) } diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index b4413a4447b7..63301ac49573 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -1025,7 +1025,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { return; } - int phoneId = getPhoneIdFromSubId(subId); synchronized (mRecords) { // register IBinder b = callback.asBinder(); @@ -1048,21 +1047,24 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID, // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID if (!SubscriptionManager.isValidSubscriptionId(subId)) { + if (DBG) { + log("invalid subscription id, use default id"); + } r.subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; } else {//APP specify subID r.subId = subId; } - r.phoneId = phoneId; + r.phoneId = getPhoneIdFromSubId(r.subId); r.eventList = events; if (DBG) { - log("listen: Register r=" + r + " r.subId=" + r.subId + " phoneId=" + phoneId); + log("listen: Register r=" + r + " r.subId=" + r.subId + " r.phoneId=" + r.phoneId); } - if (notifyNow && validatePhoneId(phoneId)) { + if (notifyNow && validatePhoneId(r.phoneId)) { if (events.contains(TelephonyCallback.EVENT_SERVICE_STATE_CHANGED)){ try { - if (VDBG) log("listen: call onSSC state=" + mServiceState[phoneId]); - ServiceState rawSs = new ServiceState(mServiceState[phoneId]); + if (VDBG) log("listen: call onSSC state=" + mServiceState[r.phoneId]); + ServiceState rawSs = new ServiceState(mServiceState[r.phoneId]); if (checkFineLocationAccess(r, Build.VERSION_CODES.Q)) { r.callback.onServiceStateChanged(rawSs); } else if (checkCoarseLocationAccess(r, Build.VERSION_CODES.Q)) { @@ -1078,8 +1080,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if (events.contains(TelephonyCallback.EVENT_SIGNAL_STRENGTH_CHANGED)) { try { - if (mSignalStrength[phoneId] != null) { - int gsmSignalStrength = mSignalStrength[phoneId] + if (mSignalStrength[r.phoneId] != null) { + int gsmSignalStrength = mSignalStrength[r.phoneId] .getGsmSignalStrength(); r.callback.onSignalStrengthChanged((gsmSignalStrength == 99 ? -1 : gsmSignalStrength)); @@ -1092,7 +1094,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { TelephonyCallback.EVENT_MESSAGE_WAITING_INDICATOR_CHANGED)) { try { r.callback.onMessageWaitingIndicatorChanged( - mMessageWaiting[phoneId]); + mMessageWaiting[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } @@ -1101,7 +1103,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { TelephonyCallback.EVENT_CALL_FORWARDING_INDICATOR_CHANGED)) { try { r.callback.onCallForwardingIndicatorChanged( - mCallForwarding[phoneId]); + mCallForwarding[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } @@ -1109,11 +1111,11 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if (validateEventAndUserLocked( r, TelephonyCallback.EVENT_CELL_LOCATION_CHANGED)) { try { - if (DBG_LOC) log("listen: mCellIdentity = " + mCellIdentity[phoneId]); + if (DBG_LOC) log("listen: mCellIdentity = " + mCellIdentity[r.phoneId]); if (checkCoarseLocationAccess(r, Build.VERSION_CODES.BASE) && checkFineLocationAccess(r, Build.VERSION_CODES.Q)) { // null will be translated to empty CellLocation object in client. - r.callback.onCellLocationChanged(mCellIdentity[phoneId]); + r.callback.onCellLocationChanged(mCellIdentity[r.phoneId]); } } catch (RemoteException ex) { remove(r.binder); @@ -1121,38 +1123,38 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if (events.contains(TelephonyCallback.EVENT_LEGACY_CALL_STATE_CHANGED)) { try { - r.callback.onLegacyCallStateChanged(mCallState[phoneId], - getCallIncomingNumber(r, phoneId)); + r.callback.onLegacyCallStateChanged(mCallState[r.phoneId], + getCallIncomingNumber(r, r.phoneId)); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_CALL_STATE_CHANGED)) { try { - r.callback.onCallStateChanged(mCallState[phoneId]); + r.callback.onCallStateChanged(mCallState[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_DATA_CONNECTION_STATE_CHANGED)) { try { - r.callback.onDataConnectionStateChanged(mDataConnectionState[phoneId], - mDataConnectionNetworkType[phoneId]); + r.callback.onDataConnectionStateChanged(mDataConnectionState[r.phoneId], + mDataConnectionNetworkType[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_DATA_ACTIVITY_CHANGED)) { try { - r.callback.onDataActivity(mDataActivity[phoneId]); + r.callback.onDataActivity(mDataActivity[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_SIGNAL_STRENGTHS_CHANGED)) { try { - if (mSignalStrength[phoneId] != null) { - r.callback.onSignalStrengthsChanged(mSignalStrength[phoneId]); + if (mSignalStrength[r.phoneId] != null) { + r.callback.onSignalStrengthsChanged(mSignalStrength[r.phoneId]); } } catch (RemoteException ex) { remove(r.binder); @@ -1162,8 +1164,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { TelephonyCallback.EVENT_ALWAYS_REPORTED_SIGNAL_STRENGTH_CHANGED)) { updateReportSignalStrengthDecision(r.subId); try { - if (mSignalStrength[phoneId] != null) { - r.callback.onSignalStrengthsChanged(mSignalStrength[phoneId]); + if (mSignalStrength[r.phoneId] != null) { + r.callback.onSignalStrengthsChanged(mSignalStrength[r.phoneId]); } } catch (RemoteException ex) { remove(r.binder); @@ -1172,11 +1174,13 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if (validateEventAndUserLocked( r, TelephonyCallback.EVENT_CELL_INFO_CHANGED)) { try { - if (DBG_LOC) log("listen: mCellInfo[" + phoneId + "] = " - + mCellInfo.get(phoneId)); + if (DBG_LOC) { + log("listen: mCellInfo[" + r.phoneId + "] = " + + mCellInfo.get(r.phoneId)); + } if (checkCoarseLocationAccess(r, Build.VERSION_CODES.BASE) && checkFineLocationAccess(r, Build.VERSION_CODES.Q)) { - r.callback.onCellInfoChanged(mCellInfo.get(phoneId)); + r.callback.onCellInfoChanged(mCellInfo.get(r.phoneId)); } } catch (RemoteException ex) { remove(r.binder); @@ -1184,22 +1188,22 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if (events.contains(TelephonyCallback.EVENT_PRECISE_CALL_STATE_CHANGED)) { try { - r.callback.onPreciseCallStateChanged(mPreciseCallState[phoneId]); + r.callback.onPreciseCallStateChanged(mPreciseCallState[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_CALL_DISCONNECT_CAUSE_CHANGED)) { try { - r.callback.onCallDisconnectCauseChanged(mCallDisconnectCause[phoneId], - mCallPreciseDisconnectCause[phoneId]); + r.callback.onCallDisconnectCauseChanged(mCallDisconnectCause[r.phoneId], + mCallPreciseDisconnectCause[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_IMS_CALL_DISCONNECT_CAUSE_CHANGED)) { try { - r.callback.onImsCallDisconnectCauseChanged(mImsReasonInfo.get(phoneId)); + r.callback.onImsCallDisconnectCauseChanged(mImsReasonInfo.get(r.phoneId)); } catch (RemoteException ex) { remove(r.binder); } @@ -1208,7 +1212,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { TelephonyCallback.EVENT_PRECISE_DATA_CONNECTION_STATE_CHANGED)) { try { for (PreciseDataConnectionState pdcs - : mPreciseDataConnectionStates.get(phoneId).values()) { + : mPreciseDataConnectionStates.get(r.phoneId).values()) { r.callback.onPreciseDataConnectionStateChanged(pdcs); } } catch (RemoteException ex) { @@ -1225,29 +1229,29 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if (events.contains(TelephonyCallback.EVENT_VOICE_ACTIVATION_STATE_CHANGED)) { try { r.callback.onVoiceActivationStateChanged( - mVoiceActivationState[phoneId]); + mVoiceActivationState[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_DATA_ACTIVATION_STATE_CHANGED)) { try { - r.callback.onDataActivationStateChanged(mDataActivationState[phoneId]); + r.callback.onDataActivationStateChanged(mDataActivationState[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_USER_MOBILE_DATA_STATE_CHANGED)) { try { - r.callback.onUserMobileDataStateChanged(mUserMobileDataState[phoneId]); + r.callback.onUserMobileDataStateChanged(mUserMobileDataState[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_DISPLAY_INFO_CHANGED)) { try { - if (mTelephonyDisplayInfos[phoneId] != null) { - r.callback.onDisplayInfoChanged(mTelephonyDisplayInfos[phoneId]); + if (mTelephonyDisplayInfos[r.phoneId] != null) { + r.callback.onDisplayInfoChanged(mTelephonyDisplayInfos[r.phoneId]); } } catch (RemoteException ex) { remove(r.binder); @@ -1284,20 +1288,20 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if (events.contains(TelephonyCallback.EVENT_SRVCC_STATE_CHANGED)) { try { - r.callback.onSrvccStateChanged(mSrvccState[phoneId]); + r.callback.onSrvccStateChanged(mSrvccState[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED)) { try { - r.callback.onCallAttributesChanged(mCallAttributes[phoneId]); + r.callback.onCallAttributesChanged(mCallAttributes[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if (events.contains(TelephonyCallback.EVENT_BARRING_INFO_CHANGED)) { - BarringInfo barringInfo = mBarringInfo.get(phoneId); + BarringInfo barringInfo = mBarringInfo.get(r.phoneId); BarringInfo biNoLocation = barringInfo != null ? barringInfo.createLocationInfoSanitizedCopy() : null; if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo); @@ -1315,8 +1319,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { r.callback.onPhysicalChannelConfigChanged( shouldSanitizeLocationForPhysicalChannelConfig(r) ? getLocationSanitizedConfigs( - mPhysicalChannelConfigs.get(phoneId)) - : mPhysicalChannelConfigs.get(phoneId)); + mPhysicalChannelConfigs.get(r.phoneId)) + : mPhysicalChannelConfigs.get(r.phoneId)); } catch (RemoteException ex) { remove(r.binder); } @@ -1325,7 +1329,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { TelephonyCallback.EVENT_DATA_ENABLED_CHANGED)) { try { r.callback.onDataEnabledChanged( - mIsDataEnabled[phoneId], mDataEnabledReason[phoneId]); + mIsDataEnabled[r.phoneId], mDataEnabledReason[r.phoneId]); } catch (RemoteException ex) { remove(r.binder); } @@ -1333,9 +1337,9 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if (events.contains( TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED)) { try { - if (mLinkCapacityEstimateLists.get(phoneId) != null) { + if (mLinkCapacityEstimateLists.get(r.phoneId) != null) { r.callback.onLinkCapacityEstimateChanged(mLinkCapacityEstimateLists - .get(phoneId)); + .get(r.phoneId)); } } catch (RemoteException ex) { remove(r.binder); @@ -1577,7 +1581,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_SERVICE_STATE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { ServiceState stateToSend; @@ -1639,7 +1643,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if ((activationType == SIM_ACTIVATION_TYPE_VOICE) && r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_VOICE_ACTIVATION_STATE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { if (DBG) { log("notifyVoiceActivationStateForPhoneId: callback.onVASC r=" + r + " subId=" + subId + " phoneId=" + phoneId @@ -1650,7 +1654,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if ((activationType == SIM_ACTIVATION_TYPE_DATA) && r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_DATA_ACTIVATION_STATE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { if (DBG) { log("notifyDataActivationStateForPhoneId: callback.onDASC r=" + r + " subId=" + subId + " phoneId=" + phoneId @@ -1692,7 +1696,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { TelephonyCallback.EVENT_SIGNAL_STRENGTHS_CHANGED) || r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_ALWAYS_REPORTED_SIGNAL_STRENGTH_CHANGED)) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { if (DBG) { log("notifySignalStrengthForPhoneId: callback.onSsS r=" + r @@ -1706,7 +1710,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_SIGNAL_STRENGTH_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { int gsmSignalStrength = signalStrength.getGsmSignalStrength(); int ss = (gsmSignalStrength == 99 ? -1 : gsmSignalStrength); @@ -1753,7 +1757,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_CARRIER_NETWORK_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onCarrierNetworkChange(active); } catch (RemoteException ex) { @@ -1785,7 +1789,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (validateEventAndUserLocked( r, TelephonyCallback.EVENT_CELL_INFO_CHANGED) - && idMatch(r.subId, subId, phoneId) + && idMatch(r, subId, phoneId) && (checkCoarseLocationAccess(r, Build.VERSION_CODES.BASE) && checkFineLocationAccess(r, Build.VERSION_CODES.Q))) { try { @@ -1819,7 +1823,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_MESSAGE_WAITING_INDICATOR_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onMessageWaitingIndicatorChanged(mwi); } catch (RemoteException ex) { @@ -1846,7 +1850,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_USER_MOBILE_DATA_STATE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onUserMobileDataStateChanged(state); } catch (RemoteException ex) { @@ -1885,7 +1889,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_DISPLAY_INFO_CHANGED) - && idMatchWithoutDefaultPhoneCheck(r.subId, subId)) { + && idMatch(r, subId, phoneId)) { try { if (!mConfigurationProvider.isDisplayInfoNrAdvancedSupported( r.callingPackage, Binder.getCallingUserHandle())) { @@ -1937,7 +1941,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_CALL_FORWARDING_INDICATOR_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onCallForwardingIndicatorChanged(cfi); } catch (RemoteException ex) { @@ -1966,7 +1970,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { // Notify by correct subId. if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_DATA_ACTIVITY_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onDataActivity(state); } catch (RemoteException ex) { @@ -2014,7 +2018,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_DATA_CONNECTION_STATE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { if (DBG) { log("Notify data connection state changed on sub: " + subId); @@ -2039,7 +2043,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_PRECISE_DATA_CONNECTION_STATE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onPreciseDataConnectionStateChanged(preciseState); } catch (RemoteException ex) { @@ -2086,7 +2090,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (validateEventAndUserLocked( r, TelephonyCallback.EVENT_CELL_LOCATION_CHANGED) - && idMatch(r.subId, subId, phoneId) + && idMatch(r, subId, phoneId) && (checkCoarseLocationAccess(r, Build.VERSION_CODES.BASE) && checkFineLocationAccess(r, Build.VERSION_CODES.Q))) { try { @@ -2140,7 +2144,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_PRECISE_CALL_STATE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onPreciseCallStateChanged(mPreciseCallState[phoneId]); } catch (RemoteException ex) { @@ -2149,7 +2153,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if (notifyCallAttributes && r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onCallAttributesChanged(mCallAttributes[phoneId]); } catch (RemoteException ex) { @@ -2174,7 +2178,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_CALL_DISCONNECT_CAUSE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onCallDisconnectCauseChanged(mCallDisconnectCause[phoneId], mCallPreciseDisconnectCause[phoneId]); @@ -2199,7 +2203,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_IMS_CALL_DISCONNECT_CAUSE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { if (DBG_LOC) { log("notifyImsCallDisconnectCause: mImsReasonInfo=" @@ -2231,7 +2235,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_SRVCC_STATE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { if (DBG_LOC) { log("notifySrvccStateChanged: mSrvccState=" + state + " r=" + r); @@ -2260,7 +2264,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if ((r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_OEM_HOOK_RAW)) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onOemHookRawEvent(rawData); } catch (RemoteException ex) { @@ -2340,7 +2344,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_RADIO_POWER_STATE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onRadioPowerStateChanged(state); } catch (RemoteException ex) { @@ -2369,7 +2373,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_EMERGENCY_NUMBER_LIST_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onEmergencyNumberListChanged(mEmergencyNumberList); if (VDBG) { @@ -2456,7 +2460,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onCallAttributesChanged(mCallAttributes[phoneId]); } catch (RemoteException ex) { @@ -2487,7 +2491,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_REGISTRATION_FAILURE) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onRegistrationFailed( checkFineLocationAccess(r, Build.VERSION_CODES.BASE) @@ -2530,7 +2534,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_BARRING_INFO_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { if (DBG_LOC) { log("notifyBarringInfo: mBarringInfo=" @@ -2575,7 +2579,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_PHYSICAL_CHANNEL_CONFIG_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { if (DBG_LOC) { log("notifyPhysicalChannelConfig: mPhysicalChannelConfigs=" @@ -2642,7 +2646,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_DATA_ENABLED_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onDataEnabledChanged(enabled, reason); } catch (RemoteException ex) { @@ -2677,7 +2681,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_ALLOWED_NETWORK_TYPE_LIST_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { if (VDBG) { log("notifyAllowedNetworkTypesChanged: reason= " + reason @@ -2719,7 +2723,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { for (Record r : mRecords) { if (r.matchTelephonyCallbackEvent( TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED) - && idMatch(r.subId, subId, phoneId)) { + && idMatch(r, subId, phoneId)) { try { r.callback.onLinkCapacityEstimateChanged(linkCapacityEstimateList); } catch (RemoteException ex) { @@ -3169,33 +3173,24 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } /** - * If the registrant specified a subId, then we should only notify it if subIds match. - * If the registrant registered with DEFAULT subId, we should notify only when the related subId - * is default subId (which could be INVALID if there's no default subId). + * Match the sub id or phone id of the event to the record * - * This should be the correct way to check record ID match. in idMatch the record's phoneId is - * speculated based on subId passed by the registrant so it's not a good reference. - * But to avoid triggering potential regression only replace idMatch with it when an issue with - * idMatch is reported. Eventually this should replace all instances of idMatch. + * We follow the rules below: + * 1) If sub id of the event is invalid, phone id should be used. + * 2) The event on default sub should be notified to the records + * which register the default sub id. + * 3) Sub id should be exactly matched for all other cases. */ - private boolean idMatchWithoutDefaultPhoneCheck(int subIdInRecord, int subIdToNotify) { - if (subIdInRecord == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { - return (subIdToNotify == mDefaultSubId); - } else { - return (subIdInRecord == subIdToNotify); - } - } - - boolean idMatch(int rSubId, int subId, int phoneId) { + boolean idMatch(Record r, int subId, int phoneId) { - if(subId < 0) { - // Invalid case, we need compare phoneId with default one. - return (mDefaultPhoneId == phoneId); + if (subId < 0) { + // Invalid case, we need compare phoneId. + return (r.phoneId == phoneId); } - if(rSubId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { + if (r.subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { return (subId == mDefaultSubId); } else { - return (rSubId == subId); + return (r.subId == subId); } } diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java index 5797b061f2d0..5fc301e60b9d 100644 --- a/services/core/java/com/android/server/display/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/DisplayModeDirector.java @@ -53,6 +53,7 @@ import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; +import android.util.SparseIntArray; import android.view.Display; import android.view.DisplayInfo; @@ -77,7 +78,6 @@ import java.util.List; import java.util.Locale; import java.util.Objects; - /** * The DisplayModeDirector is responsible for determining what modes are allowed to be automatically * picked by the system based on system-wide and display-specific configuration. @@ -92,6 +92,8 @@ public class DisplayModeDirector { private static final int MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED = 4; private static final int MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED = 5; private static final int MSG_HIGH_BRIGHTNESS_THRESHOLDS_CHANGED = 6; + private static final int MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED = 7; + private static final int MSG_REFRESH_RATE_IN_HBM_HDR_CHANGED = 8; // Special ID used to indicate that given vote is to be applied globally, rather than to a // specific display. @@ -161,9 +163,10 @@ public class DisplayModeDirector { } }; mSensorObserver = new SensorObserver(context, ballotBox, injector); - mHbmObserver = new HbmObserver(injector, ballotBox, BackgroundThread.getHandler()); mSkinThermalStatusObserver = new SkinThermalStatusObserver(injector, ballotBox); mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings(); + mHbmObserver = new HbmObserver(injector, ballotBox, BackgroundThread.getHandler(), + mDeviceConfigDisplaySettings); mDeviceConfig = injector.getDeviceConfig(); mAlwaysRespectAppRequest = false; } @@ -724,6 +727,11 @@ public class DisplayModeDirector { } @VisibleForTesting + HbmObserver getHbmObserver() { + return mHbmObserver; + } + + @VisibleForTesting DesiredDisplayModeSpecs getDesiredDisplayModeSpecsWithInjectedFpsSettings( float minRefreshRate, float peakRefreshRate, float defaultRefreshRate) { synchronized (mLock) { @@ -792,6 +800,19 @@ public class DisplayModeDirector { (DesiredDisplayModeSpecsListener) msg.obj; desiredDisplayModeSpecsListener.onDesiredDisplayModeSpecsChanged(); break; + + case MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED: { + int refreshRateInHbmSunlight = msg.arg1; + mHbmObserver.onDeviceConfigRefreshRateInHbmSunlightChanged( + refreshRateInHbmSunlight); + break; + } + + case MSG_REFRESH_RATE_IN_HBM_HDR_CHANGED: { + int refreshRateInHbmHdr = msg.arg1; + mHbmObserver.onDeviceConfigRefreshRateInHbmHdrChanged(refreshRateInHbmHdr); + break; + } } } } @@ -918,16 +939,19 @@ public class DisplayModeDirector { // result is a range. public static final int PRIORITY_FLICKER_REFRESH_RATE = 1; + // High-brightness-mode may need a specific range of refresh-rates to function properly. + public static final int PRIORITY_HIGH_BRIGHTNESS_MODE = 2; + // SETTING_MIN_REFRESH_RATE is used to propose a lower bound of display refresh rate. // It votes [MIN_REFRESH_RATE, Float.POSITIVE_INFINITY] - public static final int PRIORITY_USER_SETTING_MIN_REFRESH_RATE = 2; + public static final int PRIORITY_USER_SETTING_MIN_REFRESH_RATE = 3; // APP_REQUEST_REFRESH_RATE_RANGE is used to for internal apps to limit the refresh // rate in certain cases, mostly to preserve power. // @see android.view.WindowManager.LayoutParams#preferredMinRefreshRate // @see android.view.WindowManager.LayoutParams#preferredMaxRefreshRate // It votes to [preferredMinRefreshRate, preferredMaxRefreshRate]. - public static final int PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE = 3; + public static final int PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE = 4; // We split the app request into different priorities in case we can satisfy one desire // without the other. @@ -942,27 +966,24 @@ public class DisplayModeDirector { // The preferred refresh rate is set on the main surface of the app outside of // DisplayModeDirector. // @see com.android.server.wm.WindowState#updateFrameRateSelectionPriorityIfNeeded - public static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 4; - public static final int PRIORITY_APP_REQUEST_SIZE = 5; + public static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 5; + public static final int PRIORITY_APP_REQUEST_SIZE = 6; // SETTING_PEAK_REFRESH_RATE has a high priority and will restrict the bounds of the rest // of low priority voters. It votes [0, max(PEAK, MIN)] - public static final int PRIORITY_USER_SETTING_PEAK_REFRESH_RATE = 6; + public static final int PRIORITY_USER_SETTING_PEAK_REFRESH_RATE = 7; // LOW_POWER_MODE force display to [0, 60HZ] if Settings.Global.LOW_POWER_MODE is on. - public static final int PRIORITY_LOW_POWER_MODE = 7; + public static final int PRIORITY_LOW_POWER_MODE = 8; // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the // higher priority voters' result is a range, it will fix the rate to a single choice. // It's used to avoid refresh rate switches in certain conditions which may result in the // user seeing the display flickering when the switches occur. - public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 8; + public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 9; // Force display to [0, 60HZ] if skin temperature is at or above CRITICAL. - public static final int PRIORITY_SKIN_TEMPERATURE = 9; - - // High-brightness-mode may need a specific range of refresh-rates to function properly. - public static final int PRIORITY_HIGH_BRIGHTNESS_MODE = 10; + public static final int PRIORITY_SKIN_TEMPERATURE = 10; // The proximity sensor needs the refresh rate to be locked in order to function, so this is // set to a high priority. @@ -2258,33 +2279,78 @@ public class DisplayModeDirector { * HBM that are associated with that display. Restrictions are retrieved from * DisplayManagerInternal but originate in the display-device-config file. */ - private static class HbmObserver implements DisplayManager.DisplayListener { + public static class HbmObserver implements DisplayManager.DisplayListener { private final BallotBox mBallotBox; private final Handler mHandler; - private final SparseBooleanArray mHbmEnabled = new SparseBooleanArray(); + private final SparseIntArray mHbmMode = new SparseIntArray(); private final Injector mInjector; + private final DeviceConfigDisplaySettings mDeviceConfigDisplaySettings; + private int mRefreshRateInHbmSunlight; + private int mRefreshRateInHbmHdr; private DisplayManagerInternal mDisplayManagerInternal; - HbmObserver(Injector injector, BallotBox ballotBox, Handler handler) { + HbmObserver(Injector injector, BallotBox ballotBox, Handler handler, + DeviceConfigDisplaySettings displaySettings) { mInjector = injector; mBallotBox = ballotBox; mHandler = handler; + mDeviceConfigDisplaySettings = displaySettings; } public void observe() { + mRefreshRateInHbmSunlight = mDeviceConfigDisplaySettings.getRefreshRateInHbmSunlight(); + mRefreshRateInHbmHdr = mDeviceConfigDisplaySettings.getRefreshRateInHbmHdr(); + mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); mInjector.registerDisplayListener(this, mHandler, DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED); } + /** + * @return the refresh to lock to when the device is in high brightness mode for Sunlight. + */ + @VisibleForTesting + int getRefreshRateInHbmSunlight() { + return mRefreshRateInHbmSunlight; + } + + /** + * @return the refresh to lock to when the device is in high brightness mode for HDR. + */ + @VisibleForTesting + int getRefreshRateInHbmHdr() { + return mRefreshRateInHbmHdr; + } + + /** + * Recalculates the HBM vote when the device config has been changed. + */ + public void onDeviceConfigRefreshRateInHbmSunlightChanged(int refreshRate) { + if (refreshRate != mRefreshRateInHbmSunlight) { + mRefreshRateInHbmSunlight = refreshRate; + onDeviceConfigRefreshRateInHbmChanged(); + } + } + + /** + * Recalculates the HBM vote when the device config has been changed. + */ + public void onDeviceConfigRefreshRateInHbmHdrChanged(int refreshRate) { + if (refreshRate != mRefreshRateInHbmHdr) { + mRefreshRateInHbmHdr = refreshRate; + onDeviceConfigRefreshRateInHbmChanged(); + } + } + @Override public void onDisplayAdded(int displayId) {} @Override public void onDisplayRemoved(int displayId) { mBallotBox.vote(displayId, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE, null); + mHbmMode.delete(displayId); } @Override @@ -2294,31 +2360,56 @@ public class DisplayModeDirector { // Display no longer there. Assume we'll get an onDisplayRemoved very soon. return; } - final boolean isHbmEnabled = - info.highBrightnessMode != BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF; - if (isHbmEnabled == mHbmEnabled.get(displayId)) { + final int hbmMode = info.highBrightnessMode; + if (hbmMode == mHbmMode.get(displayId)) { // no change, ignore. return; } + mHbmMode.put(displayId, hbmMode); + recalculateVotesForDisplay(displayId); + } + + private void onDeviceConfigRefreshRateInHbmChanged() { + final int[] displayIds = mHbmMode.copyKeys(); + if (displayIds != null) { + for (int id : displayIds) { + recalculateVotesForDisplay(id); + } + } + } + + private void recalculateVotesForDisplay(int displayId) { + final int hbmMode = mHbmMode.get(displayId, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF); Vote vote = null; - mHbmEnabled.put(displayId, isHbmEnabled); - if (isHbmEnabled) { - final List<RefreshRateLimitation> limits = + if (hbmMode == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT) { + // Device resource properties take priority over DisplayDeviceConfig + if (mRefreshRateInHbmSunlight > 0) { + vote = Vote.forRefreshRates(mRefreshRateInHbmSunlight, + mRefreshRateInHbmSunlight); + } else { + final List<RefreshRateLimitation> limits = mDisplayManagerInternal.getRefreshRateLimitations(displayId); - for (int i = 0; limits != null && i < limits.size(); i++) { - final RefreshRateLimitation limitation = limits.get(i); - if (limitation.type == REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE) { - vote = Vote.forRefreshRates(limitation.range.min, limitation.range.max); - break; + for (int i = 0; limits != null && i < limits.size(); i++) { + final RefreshRateLimitation limitation = limits.get(i); + if (limitation.type == REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE) { + vote = Vote.forRefreshRates(limitation.range.min, limitation.range.max); + break; + } } } } + if (hbmMode == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR + && mRefreshRateInHbmHdr > 0) { + vote = Vote.forRefreshRates(mRefreshRateInHbmHdr, mRefreshRateInHbmHdr); + } mBallotBox.vote(displayId, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE, vote); } void dumpLocked(PrintWriter pw) { pw.println(" HbmObserver"); - pw.println(" mHbmEnabled: " + mHbmEnabled); + pw.println(" mHbmMode: " + mHbmMode); + pw.println(" mRefreshRateInHbmSunlight: " + mRefreshRateInHbmSunlight); + pw.println(" mRefreshRateInHbmHdr: " + mRefreshRateInHbmHdr); } } @@ -2437,6 +2528,29 @@ public class DisplayModeDirector { return refreshRate; } + public int getRefreshRateInHbmSunlight() { + final int defaultRefreshRateInHbmSunlight = + mContext.getResources().getInteger( + R.integer.config_defaultRefreshRateInHbmSunlight); + + final int refreshRate = mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_SUNLIGHT, + defaultRefreshRateInHbmSunlight); + + return refreshRate; + } + + public int getRefreshRateInHbmHdr() { + final int defaultRefreshRateInHbmHdr = + mContext.getResources().getInteger(R.integer.config_defaultRefreshRateInHbmHdr); + + final int refreshRate = mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_HDR, + defaultRefreshRateInHbmHdr); + + return refreshRate; + } + /* * Return null if no such property */ @@ -2476,6 +2590,15 @@ public class DisplayModeDirector { .sendToTarget(); mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone, 0) .sendToTarget(); + + final int refreshRateInHbmSunlight = getRefreshRateInHbmSunlight(); + mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED, + refreshRateInHbmSunlight, 0) + .sendToTarget(); + + final int refreshRateInHbmHdr = getRefreshRateInHbmHdr(); + mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_HDR_CHANGED, refreshRateInHbmHdr, 0) + .sendToTarget(); } private int[] getIntArrayProperty(String prop) { diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 4149980c7d45..9db13ba825f4 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -608,6 +608,44 @@ public abstract class ActivityTaskManagerInternal { String packageName, int userId); /** + * Retrieves and returns the app-specific configuration for an arbitrary application specified + * by its packageName and userId. Returns null if no app-specific configuration has been set. + */ + @Nullable + public abstract PackageConfig getApplicationConfig(String packageName, + int userId); + + /** + * Holds app-specific configurations. + */ + public static class PackageConfig { + /** + * nightMode for the application, null if app-specific nightMode is not set. + */ + @Nullable + public final Integer mNightMode; + + /** + * {@link LocaleList} for the application, null if app-specific locales are not set. + */ + @Nullable + public final LocaleList mLocales; + + PackageConfig(Integer nightMode, LocaleList locales) { + mNightMode = nightMode; + mLocales = locales; + } + + /** + * Returns the string representation of the app-specific configuration. + */ + @Override + public String toString() { + return "PackageConfig: nightMode " + mNightMode + " locales " + mLocales; + } + } + + /** * An interface to update configuration for an application, and will persist override * configuration for this package. */ diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index f278e3e22982..89f7d92b2087 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -6564,6 +6564,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override + @Nullable + public ActivityTaskManagerInternal.PackageConfig getApplicationConfig(String packageName, + int userId) { + return mPackageConfigPersister.findPackageConfiguration(packageName, userId); + } + + @Override public boolean hasSystemAlertWindowPermission(int callingUid, int callingPid, String callingPackage) { return ActivityTaskManagerService.this.hasSystemAlertWindowPermission(callingUid, diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 145bdfda3ce3..11936b21bd4e 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -841,6 +841,12 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { logIfTransactionTooLarge(r.intent, r.getSavedState()); + if (r.isEmbedded()) { + // Sending TaskFragmentInfo to client to ensure the info is updated before + // the activity creation. + mService.mTaskFragmentOrganizerController.dispatchPendingInfoChangedEvent( + r.getOrganizedTaskFragment()); + } // Create activity launch transaction. final ClientTransaction clientTransaction = ClientTransaction.obtain( diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 3fc785a25454..9561de09971b 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -530,40 +530,81 @@ public class AppTransitionController { * * @return {@code true} if the transition is overridden. */ - @VisibleForTesting - boolean overrideWithTaskFragmentRemoteAnimation(@TransitionOldType int transit, + private boolean overrideWithTaskFragmentRemoteAnimation(@TransitionOldType int transit, ArraySet<Integer> activityTypes) { final ArrayList<WindowContainer> allWindows = new ArrayList<>(); allWindows.addAll(mDisplayContent.mClosingApps); allWindows.addAll(mDisplayContent.mOpeningApps); allWindows.addAll(mDisplayContent.mChangingContainers); - // Find the common TaskFragmentOrganizer of all windows. - ITaskFragmentOrganizer organizer = null; + // It should only animated by the organizer if all windows are below the same leaf Task. + Task leafTask = null; for (int i = allWindows.size() - 1; i >= 0; i--) { final ActivityRecord r = getAppFromContainer(allWindows.get(i)); if (r == null) { return false; } + // The activity may be a child of embedded Task, but we want to find the owner Task. + // As a result, find the organized TaskFragment first. final TaskFragment organizedTaskFragment = r.getOrganizedTaskFragment(); - final ITaskFragmentOrganizer curOrganizer = organizedTaskFragment != null - ? organizedTaskFragment.getTaskFragmentOrganizer() - : null; - if (curOrganizer == null) { - // All windows must below an organized TaskFragment. + // There are also cases where the Task contains non-embedded activity, such as launching + // split TaskFragments from a non-embedded activity. + // The hierarchy may looks like this: + // - Task + // - Activity + // - TaskFragment + // - Activity + // - TaskFragment + // - Activity + // We also want to have the organizer handle the transition for such case. + final Task task = organizedTaskFragment != null + ? organizedTaskFragment.getTask() + : r.getTask(); + if (task == null) { + return false; + } + // We don't want the organizer to handle transition of other non-embedded Task. + if (leafTask != null && leafTask != task) { + return false; + } + final ActivityRecord rootActivity = task.getRootActivity(); + // We don't want the organizer to handle transition when the whole app is closing. + if (rootActivity == null) { return false; } - if (organizer == null) { - organizer = curOrganizer; - } else if (!organizer.asBinder().equals(curOrganizer.asBinder())) { - // They must be controlled by the same organizer. + // We don't want the organizer to handle transition of non-embedded activity of other + // app. + if (r.getUid() != rootActivity.getUid() && !r.isEmbedded()) { return false; } + leafTask = task; + } + if (leafTask == null) { + return false; + } + + // We don't support remote animation for Task with multiple TaskFragmentOrganizers. + final ITaskFragmentOrganizer[] organizer = new ITaskFragmentOrganizer[1]; + final boolean hasMultipleOrganizers = leafTask.forAllLeafTaskFragments(taskFragment -> { + final ITaskFragmentOrganizer tfOrganizer = taskFragment.getTaskFragmentOrganizer(); + if (tfOrganizer == null) { + return false; + } + if (organizer[0] != null && !organizer[0].asBinder().equals(tfOrganizer.asBinder())) { + return true; + } + organizer[0] = tfOrganizer; + return false; + }); + if (hasMultipleOrganizers) { + ProtoLog.e(WM_DEBUG_APP_TRANSITIONS, "We don't support remote animation for" + + " Task with multiple TaskFragmentOrganizers."); + return false; } - final RemoteAnimationDefinition definition = organizer != null + final RemoteAnimationDefinition definition = organizer[0] != null ? mDisplayContent.mAtmService.mTaskFragmentOrganizerController - .getRemoteAnimationDefinition(organizer) + .getRemoteAnimationDefinition(organizer[0]) : null; final RemoteAnimationAdapter adapter = definition != null ? definition.getAdapter(transit, activityTypes) diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 971bebd8c486..225a6ea20f3d 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -442,7 +442,9 @@ public class DisplayRotation { return false; } - if (mDisplayContent.mFixedRotationTransitionListener + final RecentsAnimationController recentsAnimController = + mService.getRecentsAnimationController(); + if (recentsAnimController != null && mDisplayContent.mFixedRotationTransitionListener .isTopFixedOrientationRecentsAnimating() // If screen is off or the device is going to sleep, then still allow to update. && mService.mPolicy.okToAnimate(false /* ignoreScreenOn */)) { @@ -450,6 +452,7 @@ public class DisplayRotation { // In order to ignore its requested orientation to avoid a sensor led rotation (e.g // user rotating the device while the recents animation is running), we ignore // rotation update while the animation is running. + recentsAnimController.setCheckRotationAfterCleanup(); return false; } } diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index 1bc1d46a4230..1e7b676fbfe4 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -48,6 +48,8 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_INPUT; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.LOGTAG_INPUT_FOCUS; +import static java.lang.Integer.MAX_VALUE; + import android.annotation.Nullable; import android.graphics.Rect; import android.graphics.Region; @@ -581,10 +583,11 @@ final class InputMonitor { if (mAddRecentsAnimationInputConsumerHandle && shouldApplyRecentsInputConsumer) { if (recentsAnimationController.updateInputConsumerForApp( mRecentsAnimationInputConsumer.mWindowHandle)) { - final WindowState highestLayerWindow = - recentsAnimationController.getHighestLayerWindow(); - if (highestLayerWindow != null) { - mRecentsAnimationInputConsumer.show(mInputTransaction, highestLayerWindow); + final DisplayArea targetDA = + recentsAnimationController.getTargetAppDisplayArea(); + if (targetDA != null) { + mRecentsAnimationInputConsumer.reparent(mInputTransaction, targetDA); + mRecentsAnimationInputConsumer.show(mInputTransaction, MAX_VALUE - 1); mAddRecentsAnimationInputConsumerHandle = false; } } @@ -597,7 +600,7 @@ final class InputMonitor { rootTask.getSurfaceControl()); // We set the layer to z=MAX-1 so that it's always on top. mPipInputConsumer.reparent(mInputTransaction, rootTask); - mPipInputConsumer.show(mInputTransaction, Integer.MAX_VALUE - 1); + mPipInputConsumer.show(mInputTransaction, MAX_VALUE - 1); mAddPipInputConsumerHandle = false; } } diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index cf2afc93c419..c712c04b72ee 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -65,6 +65,10 @@ final class LetterboxUiController { private final LetterboxConfiguration mLetterboxConfiguration; private final ActivityRecord mActivityRecord; + // Taskbar expanded height. Used to determine whether to crop an app window to display rounded + // corners above the taskbar. + private float mExpandedTaskBarHeight; + private boolean mShowWallpaperForLetterboxBackground; @Nullable @@ -76,6 +80,8 @@ final class LetterboxUiController { // is created in its constructor. It shouldn't be used in this constructor but it's safe // to use it after since controller is only used in ActivityRecord. mActivityRecord = activityRecord; + mExpandedTaskBarHeight = + getResources().getDimensionPixelSize(R.dimen.taskbar_frame_height); } /** Cleans up {@link Letterbox} if it exists.*/ @@ -314,12 +320,27 @@ final class LetterboxUiController { final InsetsSource taskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); - Rect cropBounds = new Rect(mActivityRecord.getBounds()); - // Activity bounds are in screen coordinates while (0,0) for activity's surface control - // is at the top left corner of an app window so offsetting bounds accordingly. - cropBounds.offsetTo(0, 0); - // Rounded cornerners should be displayed above the taskbar. - cropBounds.bottom = Math.min(cropBounds.bottom, taskbarInsetsSource.getFrame().top); + Rect cropBounds = null; + + // Rounded corners should be displayed above the taskbar. When taskbar is hidden, + // an insets frame is equal to a navigation bar which shouldn't affect position of + // rounded corners since apps are expected to handle navigation bar inset. + // This condition checks whether the taskbar is visible. + if (taskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + cropBounds = new Rect(mActivityRecord.getBounds()); + // Activity bounds are in screen coordinates while (0,0) for activity's surface + // control is at the top left corner of an app window so offsetting bounds + // accordingly. + cropBounds.offsetTo(0, 0); + // Rounded cornerners should be displayed above the taskbar. + cropBounds.bottom = + Math.min(cropBounds.bottom, taskbarInsetsSource.getFrame().top); + if (mActivityRecord.inSizeCompatMode() + && mActivityRecord.getSizeCompatScale() < 1.0f) { + cropBounds.scale(1.0f / mActivityRecord.getSizeCompatScale()); + } + } + transaction .setWindowCrop(windowSurface, cropBounds) .setCornerRadius(windowSurface, getRoundedCorners(insetsState)); diff --git a/services/core/java/com/android/server/wm/PackageConfigPersister.java b/services/core/java/com/android/server/wm/PackageConfigPersister.java index 52eea4d3d6e1..081a53e7bd05 100644 --- a/services/core/java/com/android/server/wm/PackageConfigPersister.java +++ b/services/core/java/com/android/server/wm/PackageConfigPersister.java @@ -272,6 +272,24 @@ public class PackageConfigPersister { } } + /** + * Retrieves and returns application configuration from persisted records if it exists, else + * returns null. + */ + ActivityTaskManagerInternal.PackageConfig findPackageConfiguration(String packageName, + int userId) { + synchronized (mLock) { + PackageConfigRecord packageConfigRecord = findRecord(mModified, packageName, userId); + if (packageConfigRecord == null) { + Slog.w(TAG, "App-specific configuration not found for packageName: " + packageName + + " and userId: " + userId); + return null; + } + return new ActivityTaskManagerInternal.PackageConfig( + packageConfigRecord.mNightMode, packageConfigRecord.mLocales); + } + } + // store a changed data so we don't need to get the process static class PackageConfigRecord { final String mName; diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java index ba85c9800e56..a663c62b40e5 100644 --- a/services/core/java/com/android/server/wm/RecentsAnimationController.java +++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java @@ -123,6 +123,7 @@ public class RecentsAnimationController implements DeathRecipient { private final int mDisplayId; private boolean mWillFinishToHome = false; private final Runnable mFailsafeRunnable = this::onFailsafe; + private Runnable mCheckRotationAfterCleanup; // The recents component app token that is shown behind the visibile tasks private ActivityRecord mTargetActivityRecord; @@ -921,6 +922,24 @@ public class RecentsAnimationController implements DeathRecipient { } /** + * If the display rotation change is ignored while recents animation is running, make sure that + * the pending rotation change will be applied after the animation finishes. + */ + void setCheckRotationAfterCleanup() { + if (mCheckRotationAfterCleanup != null) return; + mCheckRotationAfterCleanup = () -> { + synchronized (mService.mGlobalLock) { + if (mDisplayContent.getDisplayRotation() + .updateRotationAndSendNewConfigIfChanged()) { + if (mTargetActivityRecord != null) { + mTargetActivityRecord.finishFixedRotationTransform(); + } + } + } + }; + } + + /** * @return Whether we should defer the cancel from a root task order change until the next app * transition. */ @@ -1007,6 +1026,10 @@ public class RecentsAnimationController implements DeathRecipient { if (mStatusBar != null) { mStatusBar.onRecentsAnimationStateChanged(false /* running */); } + if (mCheckRotationAfterCleanup != null) { + mService.mH.post(mCheckRotationAfterCleanup); + mCheckRotationAfterCleanup = null; + } } void scheduleFailsafe() { @@ -1102,21 +1125,11 @@ public class RecentsAnimationController implements DeathRecipient { return mTargetActivityRecord.findMainWindow(); } - /** - * Returns the window with the highest layer, or null if none is found. - */ - public WindowState getHighestLayerWindow() { - int highestLayer = Integer.MIN_VALUE; - Task highestLayerTask = null; - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - TaskAnimationAdapter adapter = mPendingAnimations.get(i); - int layer = adapter.mTask.getPrefixOrderIndex(); - if (layer > highestLayer) { - highestLayer = layer; - highestLayerTask = adapter.mTask; - } + DisplayArea getTargetAppDisplayArea() { + if (mTargetActivityRecord == null) { + return null; } - return highestLayerTask.getTopMostActivity().getTopChild(); + return mTargetActivityRecord.getDisplayArea(); } boolean isAnimatingTask(Task task) { diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 30d2a323aabe..d91165685c63 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -505,29 +505,46 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } for (int i = 0, n = mPendingTaskFragmentEvents.size(); i < n; i++) { PendingTaskFragmentEvent event = mPendingTaskFragmentEvents.get(i); - final ITaskFragmentOrganizer taskFragmentOrg = event.mTaskFragmentOrg; - final TaskFragment taskFragment = event.mTaskFragment; - final TaskFragmentOrganizerState state = - mTaskFragmentOrganizerState.get(taskFragmentOrg.asBinder()); - if (state == null) continue; - switch (event.mEventType) { - case PendingTaskFragmentEvent.EVENT_APPEARED: - state.onTaskFragmentAppeared(taskFragmentOrg, taskFragment); - break; - case PendingTaskFragmentEvent.EVENT_VANISHED: - state.onTaskFragmentVanished(taskFragmentOrg, taskFragment); - break; - case PendingTaskFragmentEvent.EVENT_INFO_CHANGED: - state.onTaskFragmentInfoChanged(taskFragmentOrg, taskFragment); - break; - case PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED: - state.onTaskFragmentParentInfoChanged(taskFragmentOrg, taskFragment); - break; - case PendingTaskFragmentEvent.EVENT_ERROR: - state.onTaskFragmentError(taskFragmentOrg, event.mErrorCallback, - event.mException); - } + dispatchEvent(event); } mPendingTaskFragmentEvents.clear(); } + + void dispatchPendingInfoChangedEvent(TaskFragment taskFragment) { + PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment, + PendingTaskFragmentEvent.EVENT_INFO_CHANGED); + if (event == null) { + return; + } + + dispatchEvent(event); + mPendingTaskFragmentEvents.remove(event); + } + + private void dispatchEvent(PendingTaskFragmentEvent event) { + final ITaskFragmentOrganizer taskFragmentOrg = event.mTaskFragmentOrg; + final TaskFragment taskFragment = event.mTaskFragment; + final TaskFragmentOrganizerState state = + mTaskFragmentOrganizerState.get(taskFragmentOrg.asBinder()); + if (state == null) { + return; + } + switch (event.mEventType) { + case PendingTaskFragmentEvent.EVENT_APPEARED: + state.onTaskFragmentAppeared(taskFragmentOrg, taskFragment); + break; + case PendingTaskFragmentEvent.EVENT_VANISHED: + state.onTaskFragmentVanished(taskFragmentOrg, taskFragment); + break; + case PendingTaskFragmentEvent.EVENT_INFO_CHANGED: + state.onTaskFragmentInfoChanged(taskFragmentOrg, taskFragment); + break; + case PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED: + state.onTaskFragmentParentInfoChanged(taskFragmentOrg, taskFragment); + break; + case PendingTaskFragmentEvent.EVENT_ERROR: + state.onTaskFragmentError(taskFragmentOrg, event.mErrorCallback, + event.mException); + } + } } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 69313810fa93..781b53df998e 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -462,6 +462,10 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub container.onRequestedOverrideConfigurationChanged(c); } effects |= TRANSACT_EFFECTS_CLIENT_CONFIG; + if (windowMask != 0 && container.isEmbedded()) { + // Changing bounds of the embedded TaskFragments may result in lifecycle changes. + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } } if ((change.getChangeMask() & WindowContainerTransaction.Change.CHANGE_FOCUSABLE) != 0) { if (container.setFocusable(change.getFocusable())) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index c283ef07dce6..8e30f62c1b29 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -3618,6 +3618,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final CallerIdentity caller = getCallerIdentity(); Preconditions.checkCallAuthorization(hasFullCrossUsersPermission(caller, userHandle)); + Preconditions.checkCallAuthorization( + isCallingFromPackage(adminReceiver.getPackageName(), caller.getUid()) + || isSystemUid(caller)); synchronized (getLockObject()) { ActiveAdmin administrator = getActiveAdminUncheckedLocked(adminReceiver, userHandle); @@ -14079,6 +14082,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final CallerIdentity caller = getCallerIdentity(); Preconditions.checkCallAuthorization(hasFullCrossUsersPermission(caller, userHandle)); + Preconditions.checkCallAuthorization(canManageUsers(caller)); Preconditions.checkCallAuthorization(isManagedProfile(userHandle), "You can not get organization name outside a managed profile, userId = %d", userHandle); 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 7b20bf0f6bc7..63e4efc1cce0 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -7753,6 +7753,12 @@ public class DevicePolicyManagerTest extends DpmTestBase { DpmMockContext.CALLER_SYSTEM_USER_UID, admin1.getPackageName(), MODE_DEFAULT); } + @Test + public void testGetOrganizationNameForUser_calledByNonPrivilegedApp_throwsException() { + assertExpectException(SecurityException.class, "Calling identity is not authorized", + () -> dpm.getOrganizationNameForUser(UserHandle.USER_SYSTEM)); + } + private void setupVpnAuthorization(String userVpnPackage, int userVpnUid) { final AppOpsManager.PackageOps vpnOp = new AppOpsManager.PackageOps(userVpnPackage, userVpnUid, List.of(new AppOpsManager.OpEntry( diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java index 4564296810ff..0dd5c61121db 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java @@ -20,6 +20,8 @@ import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REF import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_HIGH_DISPLAY_BRIGHTNESS_THRESHOLDS; import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_LOW_AMBIENT_BRIGHTNESS_THRESHOLDS; import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_LOW_DISPLAY_BRIGHTNESS_THRESHOLDS; +import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_SUNLIGHT; +import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_HDR; import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE; import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE; @@ -1408,6 +1410,12 @@ public class DisplayModeDirectorTest { public void testHbmVoting_forHdr() { DisplayModeDirector director = createDirectorFromRefreshRateArray(new float[] {60.0f, 90.0f}, 0); + final int hbmRefreshRate = 72; + + // Specify limitation before starting DisplayModeDirector to avoid waiting on property + // propagation + mInjector.getDeviceConfig().setRefreshRateInHbmHdr(hbmRefreshRate); + director.start(createMockSensorManager()); ArgumentCaptor<DisplayListener> captor = @@ -1432,7 +1440,7 @@ public class DisplayModeDirectorTest { new BrightnessInfo(0.45f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR)); listener.onDisplayChanged(DISPLAY_ID); vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE); - assertVoteForRefreshRate(vote, 60.f); + assertVoteForRefreshRate(vote, hbmRefreshRate); // Turn off HBM when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn( @@ -1443,6 +1451,44 @@ public class DisplayModeDirectorTest { } @Test + public void testHbmObserverGetsUpdatedRefreshRateInHbmSunlight() { + DisplayModeDirector director = + createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, /* baseModeId= */ 0); + + final int initialRefreshRate = 60; + mInjector.getDeviceConfig().setRefreshRateInHbmSunlight(initialRefreshRate); + director.start(createMockSensorManager()); + assertThat(director.getHbmObserver().getRefreshRateInHbmSunlight()) + .isEqualTo(initialRefreshRate); + + final int updatedRefreshRate = 90; + mInjector.getDeviceConfig().setRefreshRateInHbmSunlight(updatedRefreshRate); + // Need to wait for the property change to propagate to the main thread. + waitForIdleSync(); + assertThat(director.getHbmObserver().getRefreshRateInHbmSunlight()) + .isEqualTo(updatedRefreshRate); + } + + @Test + public void testHbmObserverGetsUpdatedRefreshRateInHbmHdr() { + DisplayModeDirector director = + createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, /* baseModeId= */ 0); + + final int initialRefreshRate = 60; + mInjector.getDeviceConfig().setRefreshRateInHbmHdr(initialRefreshRate); + director.start(createMockSensorManager()); + assertThat(director.getHbmObserver().getRefreshRateInHbmHdr()) + .isEqualTo(initialRefreshRate); + + final int updatedRefreshRate = 90; + mInjector.getDeviceConfig().setRefreshRateInHbmHdr(updatedRefreshRate); + // Need to wait for the property change to propagate to the main thread. + waitForIdleSync(); + assertThat(director.getHbmObserver().getRefreshRateInHbmHdr()) + .isEqualTo(updatedRefreshRate); + } + + @Test public void testHbmVoting_forSunlight() { DisplayModeDirector director = createDirectorFromRefreshRateArray(new float[] {60.0f, 90.0f}, 0); @@ -1455,11 +1501,12 @@ public class DisplayModeDirectorTest { | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED)); DisplayListener listener = captor.getValue(); + final int initialRefreshRate = 60; // Specify Limitation when(mDisplayManagerInternalMock.getRefreshRateLimitations(DISPLAY_ID)).thenReturn( List.of(new RefreshRateLimitation( DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE, - 60.f, 60.f))); + initialRefreshRate, initialRefreshRate))); // Verify that there is no HBM vote initially Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE); @@ -1470,7 +1517,39 @@ public class DisplayModeDirectorTest { new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT)); listener.onDisplayChanged(DISPLAY_ID); vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE); - assertVoteForRefreshRate(vote, 60.f); + assertVoteForRefreshRate(vote, initialRefreshRate); + + // Change refresh rate vote value through DeviceConfig, ensure it takes precedence + final int updatedRefreshRate = 90; + mInjector.getDeviceConfig().setRefreshRateInHbmSunlight(updatedRefreshRate); + // Need to wait for the property change to propagate to the main thread. + waitForIdleSync(); + assertThat(director.getHbmObserver().getRefreshRateInHbmSunlight()) + .isEqualTo(updatedRefreshRate); + vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE); + assertVoteForRefreshRate(vote, updatedRefreshRate); + + // Turn off HBM + when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn( + new BrightnessInfo(0.43f, 0.1f, 0.8f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF)); + listener.onDisplayChanged(DISPLAY_ID); + vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE); + assertNull(vote); + + // Turn HBM on again and ensure the updated vote value stuck + when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn( + new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT)); + listener.onDisplayChanged(DISPLAY_ID); + vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE); + assertVoteForRefreshRate(vote, updatedRefreshRate); + + // Reset DeviceConfig refresh rate, ensure vote falls back to the initial value + mInjector.getDeviceConfig().setRefreshRateInHbmSunlight(0); + // Need to wait for the property change to propagate to the main thread. + waitForIdleSync(); + assertThat(director.getHbmObserver().getRefreshRateInHbmSunlight()).isEqualTo(0); + vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE); + assertVoteForRefreshRate(vote, initialRefreshRate); // Turn off HBM when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn( @@ -1518,6 +1597,63 @@ public class DisplayModeDirectorTest { assertNull(vote); } + private void setHbmAndAssertRefreshRate( + DisplayModeDirector director, DisplayListener listener, int mode, float rr) { + when(mInjector.getBrightnessInfo(DISPLAY_ID)) + .thenReturn(new BrightnessInfo(1.0f, 0.0f, 1.0f, mode)); + listener.onDisplayChanged(DISPLAY_ID); + + final Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE); + if (Float.isNaN(rr)) { + assertNull(vote); + } else { + assertVoteForRefreshRate(vote, rr); + } + } + + @Test + public void testHbmVoting_forSunlightAndHdr() { + DisplayModeDirector director = + createDirectorFromRefreshRateArray(new float[] {60.0f, 90.0f}, 0); + + // Specify HDR limitation before starting DisplayModeDirector to avoid waiting on property + // propagation + final int hdrRr = 60; + mInjector.getDeviceConfig().setRefreshRateInHbmHdr(hdrRr); + director.start(createMockSensorManager()); + + ArgumentCaptor<DisplayListener> captor = ArgumentCaptor.forClass(DisplayListener.class); + verify(mInjector).registerDisplayListener(captor.capture(), any(Handler.class), + eq(DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS + | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED)); + DisplayListener listener = captor.getValue(); + + // Specify Sunlight limitations + final float sunlightRr = 90.0f; + when(mDisplayManagerInternalMock.getRefreshRateLimitations(DISPLAY_ID)) + .thenReturn(List.of(new RefreshRateLimitation( + DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE, sunlightRr, + sunlightRr))); + + // Verify that there is no HBM vote initially + Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE); + assertNull(vote); + + // Verify all state transitions + setHbmAndAssertRefreshRate( + director, listener, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT, sunlightRr); + setHbmAndAssertRefreshRate( + director, listener, BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR, hdrRr); + setHbmAndAssertRefreshRate( + director, listener, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, Float.NaN); + setHbmAndAssertRefreshRate( + director, listener, BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR, hdrRr); + setHbmAndAssertRefreshRate( + director, listener, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT, sunlightRr); + setHbmAndAssertRefreshRate( + director, listener, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, Float.NaN); + } + @Test public void testHbmVoting_RemovedDisplay() { DisplayModeDirector director = @@ -1622,6 +1758,16 @@ public class DisplayModeDirectorTest { String.valueOf(fps)); } + void setRefreshRateInHbmSunlight(int fps) { + putPropertyAndNotify(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + KEY_REFRESH_RATE_IN_HBM_SUNLIGHT, String.valueOf(fps)); + } + + void setRefreshRateInHbmHdr(int fps) { + putPropertyAndNotify(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + KEY_REFRESH_RATE_IN_HBM_HDR, String.valueOf(fps)); + } + void setLowDisplayBrightnessThresholds(int[] brightnessThresholds) { String thresholds = toPropertyValue(brightnessThresholds); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java index 764f63dcd013..5de4fcbb5aa7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java @@ -33,6 +33,7 @@ import static com.android.server.wm.ActivityRecord.State.STOPPING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -748,6 +749,92 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase { assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive()); } + @Test + public void testPackageConfigUpdate_localesNotSet_localeConfigRetrievedNull() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, + DEFAULT_USER_ID); + WindowProcessController wpc = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), wpc); + mAtm.mInternal.onProcessAdded(wpc); + + ActivityTaskManagerInternal.PackageConfig appSpecificConfig = mAtm.mInternal + .getApplicationConfig(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + // when no configuration is set we get a null object. + assertNull(appSpecificConfig); + + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(DEFAULT_PACKAGE_NAME, + DEFAULT_USER_ID); + packageConfigUpdater.setNightMode(Configuration.UI_MODE_NIGHT_YES).commit(); + + ActivityTaskManagerInternal.PackageConfig appSpecificConfig2 = mAtm.mInternal + .getApplicationConfig(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertNotNull(appSpecificConfig2); + assertNull(appSpecificConfig2.mLocales); + assertEquals(appSpecificConfig2.mNightMode.intValue(), Configuration.UI_MODE_NIGHT_YES); + } + + @Test + public void testPackageConfigUpdate_appNotRunning_configSuccessfullyApplied() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, + DEFAULT_USER_ID); + + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(DEFAULT_PACKAGE_NAME, + DEFAULT_USER_ID); + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")).commit(); + + // Verifies if the persisted app-specific configuration is same as the committed + // configuration. + ActivityTaskManagerInternal.PackageConfig appSpecificConfig = mAtm.mInternal + .getApplicationConfig(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertNotNull(appSpecificConfig); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB"), appSpecificConfig.mLocales); + + // Verifies if the persisted configuration for an arbitrary app is applied correctly when + // a new WindowProcessController is created for it. + WindowProcessController wpcAfterConfigChange = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange.getConfiguration().getLocales()); + } + + @Test + public void testPackageConfigUpdate_appRunning_configSuccessfullyApplied() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, + DEFAULT_USER_ID); + WindowProcessController wpc = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), wpc); + mAtm.mInternal.onProcessAdded(wpc); + + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(DEFAULT_PACKAGE_NAME, + DEFAULT_USER_ID); + + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")).commit(); + + ActivityTaskManagerInternal.PackageConfig appSpecificConfig = mAtm.mInternal + .getApplicationConfig(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + + // Verifies if the persisted app-specific configuration is same as the committed + // configuration. + assertNotNull(appSpecificConfig); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB"), appSpecificConfig.mLocales); + + // Verifies if the committed configuration is successfully applied to the required + // application while it is currently running. + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpc.getConfiguration().getLocales()); + } + private WindowProcessController createWindowProcessController(String packageName, int userId) { WindowProcessListener mMockListener = Mockito.mock(WindowProcessListener.class); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java index d6d7f07b2ef3..5fa76bb2e25b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java @@ -27,6 +27,8 @@ import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE; import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE; import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; +import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; +import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -44,6 +46,7 @@ import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import android.annotation.Nullable; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; @@ -763,59 +766,148 @@ public class AppTransitionControllerTest extends WindowTestsBase { } @Test - public void testGetRemoteAnimationOverrideTaskFragmentOrganizer() { - // TaskFragmentOrganizer registers remote animation. + public void testOverrideTaskFragmentAdapter_overrideWithEmbeddedActivity() { final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final ITaskFragmentOrganizer iOrganizer = - ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()); - final RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( new TestRemoteAnimationRunner(), 10, 1); - definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, adapter); - mAtm.mTaskFragmentOrganizerController.registerOrganizer(iOrganizer); - mAtm.mTaskFragmentOrganizerController.registerRemoteAnimations(iOrganizer, definition); + setupTaskFragmentRemoteAnimation(organizer, adapter); // Create a TaskFragment with embedded activity. - final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(createTask(mDisplayContent)) - .createActivityCount(1) - .setOrganizer(organizer) - .build(); + final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity( + createTask(mDisplayContent), organizer); final ActivityRecord activity = taskFragment.getTopMostActivity(); activity.allDrawn = true; spyOn(mDisplayContent.mAppTransition); - // Prepare a transition for TaskFragment. - mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CHANGE, 0); - mDisplayContent.mOpeningApps.add(activity); - mDisplayContent.mChangingContainers.add(taskFragment); - mDisplayContent.mAppTransitionController.handleAppTransitionReady(); + // Prepare a transition. + prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - // Check if the transition has been overridden. + // Should be overridden. verify(mDisplayContent.mAppTransition) .overridePendingAppTransitionRemote(adapter, false /* sync */); } @Test + public void testOverrideTaskFragmentAdapter_overrideWithNonEmbeddedActivity() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( + new TestRemoteAnimationRunner(), 10, 1); + setupTaskFragmentRemoteAnimation(organizer, adapter); + + final Task task = createTask(mDisplayContent); + // Closing non-embedded activity. + final ActivityRecord closingActivity = createActivityRecord(task); + closingActivity.allDrawn = true; + // Opening TaskFragment with embedded activity. + final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); + final ActivityRecord openingActivity = taskFragment.getTopMostActivity(); + openingActivity.allDrawn = true; + spyOn(mDisplayContent.mAppTransition); + + // Prepare a transition. + prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); + + // Should be overridden. + verify(mDisplayContent.mAppTransition) + .overridePendingAppTransitionRemote(adapter, false /* sync */); + } + + @Test + public void testOverrideTaskFragmentAdapter_overrideEmbeddedActivityWithDiffUid() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( + new TestRemoteAnimationRunner(), 10, 1); + setupTaskFragmentRemoteAnimation(organizer, adapter); + + final Task task = createTask(mDisplayContent); + // Closing TaskFragment with embedded activity. + final TaskFragment taskFragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer); + final ActivityRecord closingActivity = taskFragment1.getTopMostActivity(); + closingActivity.allDrawn = true; + closingActivity.info.applicationInfo.uid = 12345; + // Opening TaskFragment with embedded activity with different UID. + final TaskFragment taskFragment2 = createTaskFragmentWithEmbeddedActivity(task, organizer); + final ActivityRecord openingActivity = taskFragment2.getTopMostActivity(); + openingActivity.info.applicationInfo.uid = 54321; + openingActivity.allDrawn = true; + spyOn(mDisplayContent.mAppTransition); + + // Prepare a transition. + prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment1); + + // Should be overridden. + verify(mDisplayContent.mAppTransition) + .overridePendingAppTransitionRemote(adapter, false /* sync */); + } + + @Test + public void testOverrideTaskFragmentAdapter_noOverrideWithTwoApps() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( + new TestRemoteAnimationRunner(), 10, 1); + setupTaskFragmentRemoteAnimation(organizer, adapter); + + // Closing activity in Task1. + final ActivityRecord closingActivity = createActivityRecord(mDisplayContent); + closingActivity.allDrawn = true; + // Opening TaskFragment with embedded activity in Task2. + final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity( + createTask(mDisplayContent), organizer); + final ActivityRecord openingActivity = taskFragment.getTopMostActivity(); + openingActivity.allDrawn = true; + spyOn(mDisplayContent.mAppTransition); + + // Prepare a transition for TaskFragment. + prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); + + // Should not be overridden. + verify(mDisplayContent.mAppTransition, never()) + .overridePendingAppTransitionRemote(adapter, false /* sync */); + } + + @Test + public void testOverrideTaskFragmentAdapter_noOverrideNonEmbeddedActivityWithDiffUid() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( + new TestRemoteAnimationRunner(), 10, 1); + setupTaskFragmentRemoteAnimation(organizer, adapter); + + final Task task = createTask(mDisplayContent); + // Closing TaskFragment with embedded activity. + final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); + final ActivityRecord closingActivity = taskFragment.getTopMostActivity(); + closingActivity.allDrawn = true; + closingActivity.info.applicationInfo.uid = 12345; + // Opening non-embedded activity with different UID. + final ActivityRecord openingActivity = createActivityRecord(task); + openingActivity.info.applicationInfo.uid = 54321; + openingActivity.allDrawn = true; + spyOn(mDisplayContent.mAppTransition); + + // Prepare a transition. + prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); + + // Should not be overridden + verify(mDisplayContent.mAppTransition, never()) + .overridePendingAppTransitionRemote(adapter, false /* sync */); + } + + @Test public void testTransitionGoodToGoForTaskFragments() { final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); final Task task = createTask(mDisplayContent); - final TaskFragment changeTaskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .createActivityCount(1) - .setOrganizer(organizer) - .build(); + final TaskFragment changeTaskFragment = + createTaskFragmentWithEmbeddedActivity(task, organizer); final TaskFragment emptyTaskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) .setOrganizer(organizer) .build(); changeTaskFragment.getTopMostActivity().allDrawn = true; - mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CHANGE, 0); - mDisplayContent.mChangingContainers.add(changeTaskFragment); spyOn(mDisplayContent.mAppTransition); spyOn(emptyTaskFragment); - mDisplayContent.mAppTransitionController.handleAppTransitionReady(); + prepareAndTriggerAppTransition( + null /* openingActivity */, null /* closingActivity*/, changeTaskFragment); // Transition not ready because there is an empty non-finishing TaskFragment. verify(mDisplayContent.mAppTransition, never()).goodToGo(anyInt(), any()); @@ -829,4 +921,34 @@ public class AppTransitionControllerTest extends WindowTestsBase { // removed. verify(mDisplayContent.mAppTransition).goodToGo(anyInt(), any()); } + + /** Registers remote animation for the organizer. */ + private void setupTaskFragmentRemoteAnimation(TaskFragmentOrganizer organizer, + RemoteAnimationAdapter adapter) { + final ITaskFragmentOrganizer iOrganizer = + ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()); + final RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); + definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, adapter); + definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, adapter); + definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, adapter); + mAtm.mTaskFragmentOrganizerController.registerOrganizer(iOrganizer); + mAtm.mTaskFragmentOrganizerController.registerRemoteAnimations(iOrganizer, definition); + } + + private void prepareAndTriggerAppTransition(@Nullable ActivityRecord openingActivity, + @Nullable ActivityRecord closingActivity, @Nullable TaskFragment changingTaskFragment) { + if (openingActivity != null) { + mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_OPEN, 0); + mDisplayContent.mOpeningApps.add(openingActivity); + } + if (closingActivity != null) { + mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CLOSE, 0); + mDisplayContent.mClosingApps.add(closingActivity); + } + if (changingTaskFragment != null) { + mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CHANGE, 0); + mDisplayContent.mChangingContainers.add(changingTaskFragment); + } + mDisplayContent.mAppTransitionController.handleAppTransitionReady(); + } }
\ No newline at end of file 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 e3402177140d..24bbf4682157 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1562,6 +1562,7 @@ public class DisplayContentTests extends WindowTestsBase { final ActivityRecord activity = createActivityRecord(mDisplayContent); final ActivityRecord recentsActivity = createActivityRecord(mDisplayContent); recentsActivity.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT); + doReturn(mock(RecentsAnimationController.class)).when(mWm).getRecentsAnimationController(); // Do not rotate if the recents animation is animating on top. mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity); @@ -2513,7 +2514,7 @@ public class DisplayContentTests extends WindowTestsBase { assertThat("topToBottom", actualWindows, is(reverseList(expectedWindowsBottomToTop))); } - private static int getRotatedOrientation(DisplayContent dc) { + static int getRotatedOrientation(DisplayContent dc) { return dc.mBaseDisplayWidth > dc.mBaseDisplayHeight ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE; diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java index d88fbee6ae13..a680cba8b266 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java @@ -41,8 +41,8 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; @@ -439,6 +439,22 @@ public class RecentsAnimationControllerTest extends WindowTestsBase { } @Test + public void testCheckRotationAfterCleanup() { + mWm.setRecentsAnimationController(mController); + spyOn(mDisplayContent.mFixedRotationTransitionListener); + doReturn(true).when(mDisplayContent.mFixedRotationTransitionListener) + .isTopFixedOrientationRecentsAnimating(); + // Rotation update is skipped while the recents animation is running. + assertFalse(mDisplayContent.getDisplayRotation().updateOrientation(DisplayContentTests + .getRotatedOrientation(mDefaultDisplay), false /* forceUpdate */)); + final int prevRotation = mDisplayContent.getRotation(); + mWm.cleanupRecentsAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); + waitHandlerIdle(mWm.mH); + // The display should be updated to the changed orientation after the animation is finished. + assertNotEquals(mDisplayContent.getRotation(), prevRotation); + } + + @Test public void testWallpaperHasFixedRotationApplied() { unblockDisplayRotation(mDefaultDisplay); mWm.setRecentsAnimationController(mController); 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 6626aa46e7da..8ec1bd6c5787 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -699,6 +699,15 @@ class WindowTestsBase extends SystemServiceTestsBase { return builder.build(); } + static TaskFragment createTaskFragmentWithEmbeddedActivity(@NonNull Task parentTask, + TaskFragmentOrganizer organizer) { + return new TaskFragmentBuilder(parentTask.mAtmService) + .setParentTask(parentTask) + .createActivityCount(1) + .setOrganizer(organizer) + .build(); + } + /** Creates a {@link DisplayContent} that supports IME and adds it to the system. */ DisplayContent createNewDisplay() { return createNewDisplayWithImeSupport(DISPLAY_IME_POLICY_LOCAL); diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java index 4d81b5e54470..7a424c87d1d6 100644 --- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java +++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java @@ -308,6 +308,12 @@ public final class TelephonyPermissions { return checkPrivilegedReadPermissionOrCarrierPrivilegePermission( context, subId, callingPackage, callingFeatureId, message, false, reportFailure); } + + private static void throwSecurityExceptionAsUidDoesNotHaveAccess(String message, int uid) { + throw new SecurityException(message + ": The uid " + uid + + " does not meet the requirements to access device identifiers."); + } + /** * Checks whether the app with the given pid/uid can read device identifiers. * @@ -343,9 +349,14 @@ public final class TelephonyPermissions { LegacyPermissionManager permissionManager = (LegacyPermissionManager) context.getSystemService(Context.LEGACY_PERMISSION_SERVICE); - if (permissionManager.checkDeviceIdentifierAccess(callingPackage, message, callingFeatureId, - pid, uid) == PackageManager.PERMISSION_GRANTED) { - return true; + try { + if (permissionManager.checkDeviceIdentifierAccess(callingPackage, message, + callingFeatureId, + pid, uid) == PackageManager.PERMISSION_GRANTED) { + return true; + } + } catch (SecurityException se) { + throwSecurityExceptionAsUidDoesNotHaveAccess(message, uid); } if (reportFailure) { @@ -410,8 +421,8 @@ public final class TelephonyPermissions { return false; } } - throw new SecurityException(message + ": The user " + uid - + " does not meet the requirements to access device identifiers."); + throwSecurityExceptionAsUidDoesNotHaveAccess(message, uid); + return true; } /** |