diff options
93 files changed, 2711 insertions, 896 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index e82073380394..f320b742a430 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -4453,6 +4453,10 @@ public class Notification implements Parcelable * <p>Apps targeting {@link Build.VERSION_CODES#Q} and above will have to request * a permission ({@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}) in order to * use full screen intents.</p> + * <p> + * To be launched as a full screen intent, the notification must also be posted to a + * channel with importance level set to IMPORTANCE_HIGH or higher. + * </p> * * @param intent The pending intent to launch. * @param highPriority Passing true will cause this notification to be sent diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java index 9e8703779863..90e92dbe2ab0 100644 --- a/core/java/android/hardware/camera2/params/OutputConfiguration.java +++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java @@ -159,8 +159,9 @@ public final class OutputConfiguration implements Parcelable { * * <li> For a SurfaceView output surface, the timestamp base is {@link * #TIMESTAMP_BASE_CHOREOGRAPHER_SYNCED}. The timestamp is overridden with choreographer - * pulses from the display subsystem for smoother display of camera frames. The timestamp - * is roughly in the same time base as {@link android.os.SystemClock#uptimeMillis}.</li> + * pulses from the display subsystem for smoother display of camera frames when the camera + * device runs in fixed frame rate. The timestamp is roughly in the same time base as + * {@link android.os.SystemClock#uptimeMillis}.</li> * <li> For an output surface of MediaRecorder, MediaCodec, or ImageReader with {@link * android.hardware.HardwareBuffer#USAGE_VIDEO_ENCODE} usge flag, the timestamp base is * {@link #TIMESTAMP_BASE_MONOTONIC}, which is roughly the same time base as @@ -231,7 +232,8 @@ public final class OutputConfiguration implements Parcelable { * * <p>The timestamp of the output images are overridden with choreographer pulses from the * display subsystem for smoother display of camera frames. An output target of SurfaceView - * uses this time base by default.</p> + * uses this time base by default. Note that the timestamp override is done for fixed camera + * frame rate only.</p> * * <p>This timestamp base isn't applicable to SurfaceTexture targets. SurfaceTexture's * {@link android.graphics.SurfaceTexture#updateTexImage updateTexImage} function always diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index f03b7f66cdc8..30c3d50ed8ad 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -19,6 +19,10 @@ by the resources of the app using the Shell library. --> <bool name="config_enableShellMainThread">false</bool> + <!-- Determines whether to register the shell task organizer on init. + TODO(b/238217847): This config is temporary until we refactor the base WMComponent. --> + <bool name="config_registerShellTaskOrganizerOnInit">true</bool> + <!-- Animation duration for PIP when entering. --> <integer name="config_pipEnterAnimationDuration">425</integer> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index 1c0e6f726fbf..756d80204833 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -20,6 +20,9 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; +import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; +import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; + import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; @@ -42,7 +45,6 @@ import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; import java.util.List; import java.util.Set; -import java.util.function.BiFunction; /** To run the ActivityEmbedding animations. */ class ActivityEmbeddingAnimationRunner { @@ -85,7 +87,7 @@ class ActivityEmbeddingAnimationRunner { @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Runnable animationFinishCallback) { final List<ActivityEmbeddingAnimationAdapter> adapters = - createAnimationAdapters(info, startTransaction); + createAnimationAdapters(info, startTransaction, finishTransaction); long duration = 0; for (ActivityEmbeddingAnimationAdapter adapter : adapters) { duration = Math.max(duration, adapter.getDurationHint()); @@ -129,7 +131,8 @@ class ActivityEmbeddingAnimationRunner { */ @NonNull private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters( - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) { + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { boolean isChangeTransition = false; for (TransitionInfo.Change change : info.getChanges()) { if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) { @@ -145,23 +148,25 @@ class ActivityEmbeddingAnimationRunner { return createChangeAnimationAdapters(info, startTransaction); } if (Transitions.isClosingType(info.getType())) { - return createCloseAnimationAdapters(info); + return createCloseAnimationAdapters(info, startTransaction, finishTransaction); } - return createOpenAnimationAdapters(info); + return createOpenAnimationAdapters(info, startTransaction, finishTransaction); } @NonNull private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters( - @NonNull TransitionInfo info) { - return createOpenCloseAnimationAdapters(info, true /* isOpening */, - mAnimationSpec::loadOpenAnimation); + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction, + true /* isOpening */, mAnimationSpec::loadOpenAnimation); } @NonNull private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters( - @NonNull TransitionInfo info) { - return createOpenCloseAnimationAdapters(info, false /* isOpening */, - mAnimationSpec::loadCloseAnimation); + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction, + false /* isOpening */, mAnimationSpec::loadCloseAnimation); } /** @@ -170,8 +175,9 @@ class ActivityEmbeddingAnimationRunner { */ @NonNull private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters( - @NonNull TransitionInfo info, boolean isOpening, - @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider) { + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, boolean isOpening, + @NonNull AnimationProvider animationProvider) { // We need to know if the change window is only a partial of the whole animation screen. // If so, we will need to adjust it to make the whole animation screen looks like one. final List<TransitionInfo.Change> openingChanges = new ArrayList<>(); @@ -194,7 +200,8 @@ class ActivityEmbeddingAnimationRunner { final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>(); for (TransitionInfo.Change change : openingChanges) { final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( - change, animationProvider, openingWholeScreenBounds); + info, change, startTransaction, finishTransaction, animationProvider, + openingWholeScreenBounds); if (isOpening) { adapter.overrideLayer(offsetLayer++); } @@ -202,7 +209,8 @@ class ActivityEmbeddingAnimationRunner { } for (TransitionInfo.Change change : closingChanges) { final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( - change, animationProvider, closingWholeScreenBounds); + info, change, startTransaction, finishTransaction, animationProvider, + closingWholeScreenBounds); if (!isOpening) { adapter.overrideLayer(offsetLayer++); } @@ -213,10 +221,18 @@ class ActivityEmbeddingAnimationRunner { @NonNull private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter( - @NonNull TransitionInfo.Change change, - @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider, - @NonNull Rect wholeAnimationBounds) { - final Animation animation = animationProvider.apply(change, wholeAnimationBounds); + @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull AnimationProvider animationProvider, @NonNull Rect wholeAnimationBounds) { + final Animation animation = animationProvider.get(info, change, wholeAnimationBounds); + // We may want to show a background color for open/close transition. + final int backgroundColor = getTransitionBackgroundColorIfSet(info, change, animation, + 0 /* defaultColor */); + if (backgroundColor != 0) { + addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction, + finishTransaction); + } return new ActivityEmbeddingAnimationAdapter(animation, change, change.getLeash(), wholeAnimationBounds); } @@ -322,4 +338,10 @@ class ActivityEmbeddingAnimationRunner { return ScreenshotUtils.takeScreenshot(t, screenshotChange.getLeash(), animationChange.getLeash(), cropBounds, Integer.MAX_VALUE); } + + /** To provide an {@link Animation} based on the transition infos. */ + private interface AnimationProvider { + Animation get(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, + @NonNull Rect animationBounds); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java index ad0dddf77002..eb6ac7615266 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -17,6 +17,9 @@ package com.android.wm.shell.activityembedding; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; +import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; + import android.content.Context; import android.graphics.Point; import android.graphics.Rect; @@ -33,7 +36,6 @@ import android.window.TransitionInfo; import androidx.annotation.NonNull; -import com.android.internal.R; import com.android.internal.policy.TransitionAnimation; import com.android.wm.shell.transition.Transitions; @@ -175,16 +177,20 @@ class ActivityEmbeddingAnimationSpec { } @NonNull - Animation loadOpenAnimation(@NonNull TransitionInfo.Change change, - @NonNull Rect wholeAnimationBounds) { + Animation loadOpenAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = Transitions.isOpeningType(change.getMode()); final Animation animation; - // TODO(b/207070762): - // 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit - // 2. Implement edgeExtension version - animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter - ? R.anim.task_fragment_open_enter - : R.anim.task_fragment_open_exit); + // TODO(b/207070762): Implement edgeExtension version + if (shouldShowBackdrop(info, change)) { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_clear_top_open_enter + : com.android.internal.R.anim.task_fragment_clear_top_open_exit); + } else { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_open_enter + : com.android.internal.R.anim.task_fragment_open_exit); + } // Use the whole animation bounds instead of the change bounds, so that when multiple change // targets are opening at the same time, the animation applied to each will be the same. // Otherwise, we may see gap between the activities that are launching together. @@ -195,16 +201,20 @@ class ActivityEmbeddingAnimationSpec { } @NonNull - Animation loadCloseAnimation(@NonNull TransitionInfo.Change change, - @NonNull Rect wholeAnimationBounds) { + Animation loadCloseAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = Transitions.isOpeningType(change.getMode()); final Animation animation; - // TODO(b/207070762): - // 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit - // 2. Implement edgeExtension version - animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter - ? R.anim.task_fragment_close_enter - : R.anim.task_fragment_close_exit); + // TODO(b/207070762): Implement edgeExtension version + if (shouldShowBackdrop(info, change)) { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_clear_top_close_enter + : com.android.internal.R.anim.task_fragment_clear_top_close_exit); + } else { + animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter + ? com.android.internal.R.anim.task_fragment_close_enter + : com.android.internal.R.anim.task_fragment_close_exit); + } // Use the whole animation bounds instead of the change bounds, so that when multiple change // targets are closing at the same time, the animation applied to each will be the same. // Otherwise, we may see gap between the activities that are finishing together. @@ -213,4 +223,11 @@ class ActivityEmbeddingAnimationSpec { animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); return animation; } + + private boolean shouldShowBackdrop(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change) { + final Animation a = loadAttributeAnimation(info, change, WALLPAPER_TRANSITION_NONE, + mTransitionAnimation); + return a != null && a.getShowBackdrop(); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 80cdd1f79cb5..c25bbbf06dda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -29,6 +29,7 @@ import android.view.WindowManager; import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ProtoLogController; +import com.android.wm.shell.R; import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; @@ -173,6 +174,7 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static ShellTaskOrganizer provideShellTaskOrganizer( + Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, CompatUIController compatUI, @@ -180,6 +182,10 @@ public abstract class WMShellBaseModule { Optional<RecentTasksController> recentTasksOptional, @ShellMainThread ShellExecutor mainExecutor ) { + if (!context.getResources().getBoolean(R.bool.config_registerShellTaskOrganizerOnInit)) { + // TODO(b/238217847): Force override shell init if registration is disabled + shellInit = new ShellInit(mainExecutor); + } return new ShellTaskOrganizer(shellInit, shellCommandHandler, compatUI, unfoldAnimationController, recentTasksOptional, mainExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index e784261daa7e..37a50b611039 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -188,14 +188,16 @@ public abstract class WMShellModule { @ShellMainThread Choreographer mainChoreographer, ShellTaskOrganizer taskOrganizer, DisplayController displayController, - SyncTransactionQueue syncQueue) { + SyncTransactionQueue syncQueue, + @DynamicOverride DesktopModeController desktopModeController) { return new CaptionWindowDecorViewModel( context, mainHandler, mainChoreographer, taskOrganizer, displayController, - syncQueue); + syncQueue, + desktopModeController); } // @@ -318,6 +320,7 @@ public abstract class WMShellModule { ShellCommandHandler shellCommandHandler, ShellController shellController, DisplayController displayController, + PipAnimationController pipAnimationController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, PhonePipKeepClearAlgorithm pipKeepClearAlgorithm, @@ -337,11 +340,12 @@ public abstract class WMShellModule { @ShellMainThread ShellExecutor mainExecutor) { return Optional.ofNullable(PipController.create( context, shellInit, shellCommandHandler, shellController, - displayController, pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, - pipBoundsState, pipMotionHelper, pipMediaController, phonePipMenuController, - pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, - windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - displayInsetsController, oneHandedController, mainExecutor)); + displayController, pipAnimationController, pipAppOpsListener, pipBoundsAlgorithm, + pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, pipMediaController, + phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler, + pipTransitionController, windowManagerShellWrapper, taskStackListener, + pipParamsChangedForwarder, displayInsetsController, oneHandedController, + mainExecutor)); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index 9474cfe916f0..99739c457aa6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -177,6 +177,25 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll } /** + * Turn desktop mode on or off + * @param active the desired state for desktop mode setting + */ + public void setDesktopModeActive(boolean active) { + int value = active ? 1 : 0; + Settings.System.putInt(mContext.getContentResolver(), Settings.System.DESKTOP_MODE, value); + } + + /** + * Returns the windowing mode of the display area with the specified displayId. + * @param displayId + * @return + */ + public int getDisplayAreaWindowingMode(int displayId) { + return mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) + .configuration.windowConfiguration.getWindowingMode(); + } + + /** * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE} */ private final class SettingsObserver extends ContentObserver { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index b32c3eed2fb4..6728c00af51b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -195,6 +195,17 @@ public class PipAnimationController { } /** + * Returns true if the PiP window is currently being animated. + */ + public boolean isAnimating() { + PipAnimationController.PipTransitionAnimator animator = getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + return true; + } + return false; + } + + /** * Quietly cancel the animator by removing the listeners first. */ static void quietCancel(@NonNull ValueAnimator animator) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 1a52d8c395ba..f170e774739f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -324,19 +324,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return mPipTransitionController; } - /** - * Returns true if the PiP window is currently being animated. - */ - public boolean isAnimating() { - // TODO(b/183746978) move this to PipAnimationController, and inject that in PipController - PipAnimationController.PipTransitionAnimator animator = - mPipAnimationController.getCurrentAnimator(); - if (animator != null && animator.isRunning()) { - return true; - } - return false; - } - public Rect getCurrentOrAnimatingBounds() { PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getCurrentAnimator(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index bc8191d2af46..af47666efa5a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -130,6 +130,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb private DisplayController mDisplayController; private PipInputConsumer mPipInputConsumer; private WindowManagerShellWrapper mWindowManagerShellWrapper; + private PipAnimationController mPipAnimationController; private PipAppOpsListener mAppOpsListener; private PipMediaController mMediaController; private PipBoundsAlgorithm mPipBoundsAlgorithm; @@ -158,7 +159,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb return; } // if there is another animation ongoing, wait for it to finish and try again - if (mPipTaskOrganizer.isAnimating()) { + if (mPipAnimationController.isAnimating()) { mMainExecutor.removeCallbacks( mMovePipInResponseToKeepClearAreasChangeCallback); mMainExecutor.executeDelayed( @@ -368,6 +369,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb ShellCommandHandler shellCommandHandler, ShellController shellController, DisplayController displayController, + PipAnimationController pipAnimationController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, PipKeepClearAlgorithm pipKeepClearAlgorithm, @@ -392,11 +394,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb } return new PipController(context, shellInit, shellCommandHandler, shellController, - displayController, pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, - pipBoundsState, pipMotionHelper, pipMediaController, phonePipMenuController, - pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, - windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - displayInsetsController, oneHandedController, mainExecutor) + displayController, pipAnimationController, pipAppOpsListener, + pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, + pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState, + pipTouchHandler, pipTransitionController, windowManagerShellWrapper, + taskStackListener, pipParamsChangedForwarder, displayInsetsController, + oneHandedController, mainExecutor) .mImpl; } @@ -405,6 +408,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb ShellCommandHandler shellCommandHandler, ShellController shellController, DisplayController displayController, + PipAnimationController pipAnimationController, PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, PipKeepClearAlgorithm pipKeepClearAlgorithm, @@ -445,6 +449,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mMediaController = pipMediaController; mMenuController = phonePipMenuController; mTouchHandler = pipTouchHandler; + mPipAnimationController = pipAnimationController; mAppOpsListener = pipAppOpsListener; mOneHandedController = oneHandedController; mPipTransitionController = pipTransitionController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 4c927b6e84b8..dbb2948de5db 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -18,13 +18,11 @@ package com.android.wm.shell.transition; import static android.app.ActivityOptions.ANIM_CLIP_REVEAL; import static android.app.ActivityOptions.ANIM_CUSTOM; -import static android.app.ActivityOptions.ANIM_FROM_STYLE; import static android.app.ActivityOptions.ANIM_NONE; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.ActivityOptions.ANIM_SCALE_UP; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED; import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_TYPE; @@ -43,10 +41,11 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; +import static android.window.TransitionInfo.FLAG_FILLS_TASK; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; @@ -59,6 +58,10 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; +import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; +import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; +import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; +import static com.android.wm.shell.transition.TransitionAnimationHelper.sDisableCustomTaskAnimationProperty; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -74,7 +77,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Insets; import android.graphics.Paint; import android.graphics.PixelFormat; @@ -84,7 +86,6 @@ import android.graphics.drawable.Drawable; import android.hardware.HardwareBuffer; import android.os.Handler; import android.os.IBinder; -import android.os.SystemProperties; import android.os.UserHandle; import android.util.ArrayMap; import android.view.Choreographer; @@ -122,21 +123,6 @@ import java.util.function.Consumer; public class DefaultTransitionHandler implements Transitions.TransitionHandler { private static final int MAX_ANIMATION_DURATION = 3000; - /** - * Restrict ability of activities overriding transition animation in a way such that - * an activity can do it only when the transition happens within a same task. - * - * @see android.app.Activity#overridePendingTransition(int, int) - */ - private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY = - "persist.wm.disable_custom_task_animation"; - - /** - * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY - */ - static boolean sDisableCustomTaskAnimationProperty = - SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true); - private final TransactionPool mTransactionPool; private final DisplayController mDisplayController; private final Context mContext; @@ -384,8 +370,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { change.getEndAbsBounds().top - info.getRootOffset().y); // Seamless display transition doesn't need to animate. if (isSeamlessDisplayChange) continue; - if (isTask) { - // Skip non-tasks since those usually have null bounds. + if (isTask || (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY) + && !change.hasFlags(FLAG_FILLS_TASK))) { + // Update Task and embedded split window crop bounds, otherwise we may see crop + // on previous bounds during the rotation animation. startTransaction.setWindowCrop(change.getLeash(), change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); } @@ -431,22 +419,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { cornerRadius = 0; } - if (a.getShowBackdrop()) { - if (info.getAnimationOptions().getBackgroundColor() != 0) { - // If available use the background color provided through AnimationOptions - backgroundColorForTransition = - info.getAnimationOptions().getBackgroundColor(); - } else if (a.getBackdropColor() != 0) { - // Otherwise fallback on the background color provided through the animation - // definition. - backgroundColorForTransition = a.getBackdropColor(); - } else if (change.getBackgroundColor() != 0) { - // Otherwise default to the window's background color if provided through - // the theme as the background color for the animation - the top most window - // with a valid background color and showBackground set takes precedence. - backgroundColorForTransition = change.getBackgroundColor(); - } - } + backgroundColorForTransition = getTransitionBackgroundColorIfSet(info, change, a, + backgroundColorForTransition); boolean delayedEdgeExtension = false; if (!isTask && a.hasExtension()) { @@ -668,29 +642,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return edgeExtensionLayer; } - private void addBackgroundToTransition( - @NonNull SurfaceControl rootLeash, - @ColorInt int color, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction - ) { - final Color bgColor = Color.valueOf(color); - final float[] colorArray = new float[] { bgColor.red(), bgColor.green(), bgColor.blue() }; - - final SurfaceControl animationBackgroundSurface = new SurfaceControl.Builder() - .setName("Animation Background") - .setParent(rootLeash) - .setColorLayer() - .setOpaque(true) - .build(); - - startTransaction - .setLayer(animationBackgroundSurface, Integer.MIN_VALUE) - .setColor(animationBackgroundSurface, colorArray) - .show(animationBackgroundSurface); - finishTransaction.remove(animationBackgroundSurface); - } - @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @@ -704,9 +655,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } @Nullable - private Animation loadAnimation(TransitionInfo info, TransitionInfo.Change change, - int wallpaperTransit) { - Animation a = null; + private Animation loadAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, int wallpaperTransit) { + Animation a; final int type = info.getType(); final int flags = info.getFlags(); @@ -717,12 +668,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final boolean isTask = change.getTaskInfo() != null; final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); final int overrideType = options != null ? options.getType() : ANIM_NONE; - final boolean canCustomContainer = isTask ? !sDisableCustomTaskAnimationProperty : true; + final boolean canCustomContainer = !isTask || !sDisableCustomTaskAnimationProperty; final Rect endBounds = Transitions.isClosingType(changeMode) ? mRotator.getEndBoundsInStartRotation(change) : change.getEndAbsBounds(); - final boolean isDream = - isTask && change.getTaskInfo().topActivityType == ACTIVITY_TYPE_DREAM; if (info.isKeyguardGoingAway()) { a = mTransitionAnimation.loadKeyguardExitAnimation(flags, @@ -763,87 +712,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // This received a transferred starting window, so don't animate return null; } else { - int animAttr = 0; - boolean translucent = false; - if (isDream) { - if (type == TRANSIT_OPEN) { - animAttr = enter - ? R.styleable.WindowAnimation_dreamActivityOpenEnterAnimation - : R.styleable.WindowAnimation_dreamActivityOpenExitAnimation; - } else if (type == TRANSIT_CLOSE) { - animAttr = enter - ? 0 - : R.styleable.WindowAnimation_dreamActivityCloseExitAnimation; - } - } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation - : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation; - } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation - : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation; - } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation - : R.styleable.WindowAnimation_wallpaperOpenExitAnimation; - } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) { - animAttr = enter - ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation - : R.styleable.WindowAnimation_wallpaperCloseExitAnimation; - } else if (type == TRANSIT_OPEN) { - // We will translucent open animation for translucent activities and tasks. Choose - // WindowAnimation_activityOpenEnterAnimation and set translucent here, then - // TransitionAnimation loads appropriate animation later. - if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) { - translucent = true; - } - if (isTask && !translucent) { - animAttr = enter - ? R.styleable.WindowAnimation_taskOpenEnterAnimation - : R.styleable.WindowAnimation_taskOpenExitAnimation; - } else { - animAttr = enter - ? R.styleable.WindowAnimation_activityOpenEnterAnimation - : R.styleable.WindowAnimation_activityOpenExitAnimation; - } - } else if (type == TRANSIT_TO_FRONT) { - animAttr = enter - ? R.styleable.WindowAnimation_taskToFrontEnterAnimation - : R.styleable.WindowAnimation_taskToFrontExitAnimation; - } else if (type == TRANSIT_CLOSE) { - if (isTask) { - animAttr = enter - ? R.styleable.WindowAnimation_taskCloseEnterAnimation - : R.styleable.WindowAnimation_taskCloseExitAnimation; - } else { - if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) { - translucent = true; - } - animAttr = enter - ? R.styleable.WindowAnimation_activityCloseEnterAnimation - : R.styleable.WindowAnimation_activityCloseExitAnimation; - } - } else if (type == TRANSIT_TO_BACK) { - animAttr = enter - ? R.styleable.WindowAnimation_taskToBackEnterAnimation - : R.styleable.WindowAnimation_taskToBackExitAnimation; - } - - if (animAttr != 0) { - if (overrideType == ANIM_FROM_STYLE && canCustomContainer) { - a = mTransitionAnimation - .loadAnimationAttr(options.getPackageName(), options.getAnimations(), - animAttr, translucent); - } else { - a = mTransitionAnimation.loadDefaultAnimationAttr(animAttr, translucent); - } - } - - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "loadAnimation: anim=%s animAttr=0x%x type=%s isEntrance=%b", a, animAttr, - transitTypeToString(type), - enter); + a = loadAttributeAnimation(info, change, wallpaperTransit, mTransitionAnimation); } if (a != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java new file mode 100644 index 000000000000..efee6f40b53e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static android.app.ActivityOptions.ANIM_FROM_STYLE; +import static android.app.ActivityOptions.ANIM_NONE; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.view.WindowManager.transitTypeToString; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; + +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; + +import android.annotation.ColorInt; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Color; +import android.os.SystemProperties; +import android.view.SurfaceControl; +import android.view.animation.Animation; +import android.window.TransitionInfo; + +import com.android.internal.R; +import com.android.internal.policy.TransitionAnimation; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +/** The helper class that provides methods for adding styles to transition animations. */ +public class TransitionAnimationHelper { + + /** + * Restrict ability of activities overriding transition animation in a way such that + * an activity can do it only when the transition happens within a same task. + * + * @see android.app.Activity#overridePendingTransition(int, int) + */ + private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY = + "persist.wm.disable_custom_task_animation"; + + /** + * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY + */ + static final boolean sDisableCustomTaskAnimationProperty = + SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true); + + /** Loads the animation that is defined through attribute id for the given transition. */ + @Nullable + public static Animation loadAttributeAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, int wallpaperTransit, + @NonNull TransitionAnimation transitionAnimation) { + final int type = info.getType(); + final int changeMode = change.getMode(); + final int changeFlags = change.getFlags(); + final boolean enter = Transitions.isOpeningType(changeMode); + final boolean isTask = change.getTaskInfo() != null; + final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + final int overrideType = options != null ? options.getType() : ANIM_NONE; + final boolean canCustomContainer = !isTask || !sDisableCustomTaskAnimationProperty; + final boolean isDream = + isTask && change.getTaskInfo().topActivityType == ACTIVITY_TYPE_DREAM; + int animAttr = 0; + boolean translucent = false; + if (isDream) { + if (type == TRANSIT_OPEN) { + animAttr = enter + ? R.styleable.WindowAnimation_dreamActivityOpenEnterAnimation + : R.styleable.WindowAnimation_dreamActivityOpenExitAnimation; + } else if (type == TRANSIT_CLOSE) { + animAttr = enter + ? 0 + : R.styleable.WindowAnimation_dreamActivityCloseExitAnimation; + } + } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation + : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation + : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation + : R.styleable.WindowAnimation_wallpaperOpenExitAnimation; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) { + animAttr = enter + ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation + : R.styleable.WindowAnimation_wallpaperCloseExitAnimation; + } else if (type == TRANSIT_OPEN) { + // We will translucent open animation for translucent activities and tasks. Choose + // WindowAnimation_activityOpenEnterAnimation and set translucent here, then + // TransitionAnimation loads appropriate animation later. + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) { + translucent = true; + } + if (isTask && !translucent) { + animAttr = enter + ? R.styleable.WindowAnimation_taskOpenEnterAnimation + : R.styleable.WindowAnimation_taskOpenExitAnimation; + } else { + animAttr = enter + ? R.styleable.WindowAnimation_activityOpenEnterAnimation + : R.styleable.WindowAnimation_activityOpenExitAnimation; + } + } else if (type == TRANSIT_TO_FRONT) { + animAttr = enter + ? R.styleable.WindowAnimation_taskToFrontEnterAnimation + : R.styleable.WindowAnimation_taskToFrontExitAnimation; + } else if (type == TRANSIT_CLOSE) { + if (isTask) { + animAttr = enter + ? R.styleable.WindowAnimation_taskCloseEnterAnimation + : R.styleable.WindowAnimation_taskCloseExitAnimation; + } else { + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) { + translucent = true; + } + animAttr = enter + ? R.styleable.WindowAnimation_activityCloseEnterAnimation + : R.styleable.WindowAnimation_activityCloseExitAnimation; + } + } else if (type == TRANSIT_TO_BACK) { + animAttr = enter + ? R.styleable.WindowAnimation_taskToBackEnterAnimation + : R.styleable.WindowAnimation_taskToBackExitAnimation; + } + + Animation a = null; + if (animAttr != 0) { + if (overrideType == ANIM_FROM_STYLE && canCustomContainer) { + a = transitionAnimation + .loadAnimationAttr(options.getPackageName(), options.getAnimations(), + animAttr, translucent); + } else { + a = transitionAnimation.loadDefaultAnimationAttr(animAttr, translucent); + } + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "loadAnimation: anim=%s animAttr=0x%x type=%s isEntrance=%b", a, animAttr, + transitTypeToString(type), + enter); + return a; + } + + /** + * Gets the background {@link ColorInt} for the given transition animation if it is set. + * + * @param defaultColor {@link ColorInt} to return if there is no background color specified by + * the given transition animation. + */ + @ColorInt + public static int getTransitionBackgroundColorIfSet(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Animation a, + @ColorInt int defaultColor) { + if (!a.getShowBackdrop()) { + return defaultColor; + } + if (info.getAnimationOptions() != null + && info.getAnimationOptions().getBackgroundColor() != 0) { + // If available use the background color provided through AnimationOptions + return info.getAnimationOptions().getBackgroundColor(); + } else if (a.getBackdropColor() != 0) { + // Otherwise fallback on the background color provided through the animation + // definition. + return a.getBackdropColor(); + } else if (change.getBackgroundColor() != 0) { + // Otherwise default to the window's background color if provided through + // the theme as the background color for the animation - the top most window + // with a valid background color and showBackground set takes precedence. + return change.getBackgroundColor(); + } + return defaultColor; + } + + /** + * Adds the given {@code backgroundColor} as the background color to the transition animation. + */ + public static void addBackgroundToTransition(@NonNull SurfaceControl rootLeash, + @ColorInt int backgroundColor, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + if (backgroundColor == 0) { + // No background color. + return; + } + final Color bgColor = Color.valueOf(backgroundColor); + final float[] colorArray = new float[] { bgColor.red(), bgColor.green(), bgColor.blue() }; + final SurfaceControl animationBackgroundSurface = new SurfaceControl.Builder() + .setName("Animation Background") + .setParent(rootLeash) + .setColorLayer() + .setOpaque(true) + .build(); + startTransaction + .setLayer(animationBackgroundSurface, Integer.MIN_VALUE) + .setColor(animationBackgroundSurface, colorArray) + .show(animationBackgroundSurface); + finishTransaction.remove(animationBackgroundSurface); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 8b36db9204ac..9e49b51e1504 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -37,6 +37,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.DesktopModeController; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.transition.Transitions; @@ -54,6 +55,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption private final DisplayController mDisplayController; private final SyncTransactionQueue mSyncQueue; private FreeformTaskTransitionStarter mTransitionStarter; + private DesktopModeController mDesktopModeController; public CaptionWindowDecorViewModel( Context context, @@ -61,7 +63,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption Choreographer mainChoreographer, ShellTaskOrganizer taskOrganizer, DisplayController displayController, - SyncTransactionQueue syncQueue) { + SyncTransactionQueue syncQueue, + DesktopModeController desktopModeController) { mContext = context; mMainHandler = mainHandler; mMainChoreographer = mainChoreographer; @@ -69,6 +72,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption mTaskOrganizer = taskOrganizer; mDisplayController = displayController; mSyncQueue = syncQueue; + mDesktopModeController = desktopModeController; } @Override @@ -211,8 +215,10 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption } private void handleEventForMove(MotionEvent e) { - if (mTaskOrganizer.getRunningTaskInfo(mTaskId).getWindowingMode() - == WINDOWING_MODE_FULLSCREEN) { + RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + int windowingMode = mDesktopModeController + .getDisplayAreaWindowingMode(taskInfo.displayId); + if (windowingMode == WINDOWING_MODE_FULLSCREEN) { return; } switch (e.getActionMasked()) { @@ -230,8 +236,14 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { int dragPointerIdx = e.findPointerIndex(mDragPointerId); + int statusBarHeight = mDisplayController.getDisplayLayout(taskInfo.displayId) + .stableInsets().top; mDragResizeCallback.onDragResizeEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); + if (e.getRawY(dragPointerIdx) <= statusBarHeight + && windowingMode == WINDOWING_MODE_FREEFORM) { + mDesktopModeController.setDesktopModeActive(false); + } break; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index a8d3bdcb7c96..1e08f1e55797 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -48,6 +48,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; @@ -85,6 +86,7 @@ public class PipControllerTest extends ShellTestCase { @Mock private ShellCommandHandler mMockShellCommandHandler; @Mock private DisplayController mMockDisplayController; @Mock private PhonePipMenuController mMockPhonePipMenuController; + @Mock private PipAnimationController mMockPipAnimationController; @Mock private PipAppOpsListener mMockPipAppOpsListener; @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; @Mock private PhonePipKeepClearAlgorithm mMockPipKeepClearAlgorithm; @@ -117,8 +119,8 @@ public class PipControllerTest extends ShellTestCase { mShellController = spy(new ShellController(mShellInit, mMockShellCommandHandler, mMockExecutor)); mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler, - mShellController, mMockDisplayController, mMockPipAppOpsListener, - mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, + mShellController, mMockDisplayController, mMockPipAnimationController, + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, @@ -183,8 +185,8 @@ public class PipControllerTest extends ShellTestCase { ShellInit shellInit = new ShellInit(mMockExecutor); assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler, - mShellController, mMockDisplayController, mMockPipAppOpsListener, - mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, + mShellController, mMockDisplayController, mMockPipAnimationController, + mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm, mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController, mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState, mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper, diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 2aca41e41905..8e350d5012a5 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -53,8 +53,12 @@ SkiaOpenGLPipeline::~SkiaOpenGLPipeline() { } MakeCurrentResult SkiaOpenGLPipeline::makeCurrent() { - // TODO: Figure out why this workaround is needed, see b/13913604 - // In the meantime this matches the behavior of GLRenderer, so it is not a regression + // In case the surface was destroyed (e.g. a previous trimMemory call) we + // need to recreate it here. + if (!isSurfaceReady() && mNativeWindow) { + setSurface(mNativeWindow.get(), mSwapBehavior); + } + EGLint error = 0; if (!mEglManager.makeCurrent(mEglSurface, &error)) { return MakeCurrentResult::AlreadyCurrent; @@ -166,6 +170,9 @@ void SkiaOpenGLPipeline::onStop() { } bool SkiaOpenGLPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) { + mNativeWindow = surface; + mSwapBehavior = swapBehavior; + if (mEglSurface != EGL_NO_SURFACE) { mEglManager.destroySurface(mEglSurface); mEglSurface = EGL_NO_SURFACE; @@ -182,7 +189,8 @@ bool SkiaOpenGLPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBeh if (mEglSurface != EGL_NO_SURFACE) { const bool preserveBuffer = (swapBehavior != SwapBehavior::kSwap_discardBuffer); - mBufferPreserved = mEglManager.setPreserveBuffer(mEglSurface, preserveBuffer); + const bool isPreserved = mEglManager.setPreserveBuffer(mEglSurface, preserveBuffer); + ALOGE_IF(preserveBuffer != isPreserved, "Unable to match the desired swap behavior."); return true; } diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h index 186998a01745..a80c613697f2 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h @@ -61,7 +61,8 @@ protected: private: renderthread::EglManager& mEglManager; EGLSurface mEglSurface = EGL_NO_SURFACE; - bool mBufferPreserved = false; + sp<ANativeWindow> mNativeWindow; + renderthread::SwapBehavior mSwapBehavior = renderthread::SwapBehavior::kSwap_discardBuffer; }; } /* namespace skiapipeline */ diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 905d46e58014..cc2565d88d5e 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -55,7 +55,12 @@ VulkanManager& SkiaVulkanPipeline::vulkanManager() { } MakeCurrentResult SkiaVulkanPipeline::makeCurrent() { - return MakeCurrentResult::AlreadyCurrent; + // In case the surface was destroyed (e.g. a previous trimMemory call) we + // need to recreate it here. + if (!isSurfaceReady() && mNativeWindow) { + setSurface(mNativeWindow.get(), SwapBehavior::kSwap_default); + } + return isContextReady() ? MakeCurrentResult::AlreadyCurrent : MakeCurrentResult::Failed; } Frame SkiaVulkanPipeline::getFrame() { @@ -130,7 +135,11 @@ DeferredLayerUpdater* SkiaVulkanPipeline::createTextureLayer() { void SkiaVulkanPipeline::onStop() {} -bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) { +// We can safely ignore the swap behavior because VkManager will always operate +// in a mode equivalent to EGLManager::SwapBehavior::kBufferAge +bool SkiaVulkanPipeline::setSurface(ANativeWindow* surface, SwapBehavior /*swapBehavior*/) { + mNativeWindow = surface; + if (mVkSurface) { vulkanManager().destroySurface(mVkSurface); mVkSurface = nullptr; diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h index ada6af67d4a0..a6e685d08aeb 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h @@ -61,6 +61,7 @@ private: renderthread::VulkanManager& vulkanManager(); renderthread::VulkanSurface* mVkSurface = nullptr; + sp<ANativeWindow> mNativeWindow; }; } /* namespace skiapipeline */ diff --git a/libs/hwui/tests/unit/SkiaPipelineTests.cpp b/libs/hwui/tests/unit/SkiaPipelineTests.cpp index 60ae6044cd5b..7419f8fd89f1 100644 --- a/libs/hwui/tests/unit/SkiaPipelineTests.cpp +++ b/libs/hwui/tests/unit/SkiaPipelineTests.cpp @@ -404,7 +404,9 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaPipeline, context_lost) { EXPECT_TRUE(pipeline->isSurfaceReady()); renderThread.destroyRenderingContext(); EXPECT_FALSE(pipeline->isSurfaceReady()); - LOG_ALWAYS_FATAL_IF(pipeline->isSurfaceReady()); + + pipeline->makeCurrent(); + EXPECT_TRUE(pipeline->isSurfaceReady()); } RENDERTHREAD_SKIA_PIPELINE_TEST(SkiaPipeline, pictureCallback) { diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp index 1ebdc273931b..795af8a58351 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -300,7 +300,7 @@ void ASurfaceTransaction_setOnComplete(ASurfaceTransaction* aSurfaceTransaction, auto& aSurfaceControlStats = aSurfaceTransactionStats.aSurfaceControlStats; for (const auto& [surfaceControl, latchTime, acquireTimeOrFence, presentFence, - previousReleaseFence, transformHint, frameEvents] : surfaceControlStats) { + previousReleaseFence, transformHint, frameEvents, ignore] : surfaceControlStats) { ASurfaceControl* aSurfaceControl = reinterpret_cast<ASurfaceControl*>(surfaceControl.get()); aSurfaceControlStats[aSurfaceControl].acquireTimeOrFence = acquireTimeOrFence; aSurfaceControlStats[aSurfaceControl].previousReleaseFence = previousReleaseFence; @@ -650,7 +650,7 @@ void ASurfaceTransaction_setOnCommit(ASurfaceTransaction* aSurfaceTransaction, v auto& aSurfaceControlStats = aSurfaceTransactionStats.aSurfaceControlStats; for (const auto& [surfaceControl, latchTime, acquireTimeOrFence, presentFence, - previousReleaseFence, transformHint, frameEvents] : + previousReleaseFence, transformHint, frameEvents, ignore] : surfaceControlStats) { ASurfaceControl* aSurfaceControl = reinterpret_cast<ASurfaceControl*>(surfaceControl.get()); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 7927c5d460a4..eb53ea1d44f7 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -758,16 +758,23 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } public boolean isBusy() { - synchronized (mProfileLock) { - for (LocalBluetoothProfile profile : mProfiles) { - int status = getProfileConnectionState(profile); - if (status == BluetoothProfile.STATE_CONNECTING - || status == BluetoothProfile.STATE_DISCONNECTING) { - return true; - } + for (CachedBluetoothDevice memberDevice : getMemberDevice()) { + if (isBusyState(memberDevice)) { + return true; + } + } + return isBusyState(this); + } + + private boolean isBusyState(CachedBluetoothDevice device){ + for (LocalBluetoothProfile profile : device.getProfiles()) { + int status = device.getProfileConnectionState(profile); + if (status == BluetoothProfile.STATE_CONNECTING + || status == BluetoothProfile.STATE_DISCONNECTING) { + return true; } - return getBondState() == BluetoothDevice.BOND_BONDING; } + return device.getBondState() == BluetoothDevice.BOND_BONDING; } private boolean updateProfiles() { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index 79e99387b2fa..315ab0aac878 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -1069,4 +1069,80 @@ public class CachedBluetoothDeviceTest { assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice); assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); } + + @Test + public void isBusy_mainDeviceIsConnecting_returnsBusy() { + mCachedDevice.addMemberDevice(mSubCachedDevice); + updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); + updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); + when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + + updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING); + + assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); + assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); + assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); + assertThat(mCachedDevice.isBusy()).isTrue(); + } + + @Test + public void isBusy_mainDeviceIsBonding_returnsBusy() { + mCachedDevice.addMemberDevice(mSubCachedDevice); + updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); + updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); + when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + + when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING); + + assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); + assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); + assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); + assertThat(mCachedDevice.isBusy()).isTrue(); + } + + @Test + public void isBusy_memberDeviceIsConnecting_returnsBusy() { + mCachedDevice.addMemberDevice(mSubCachedDevice); + updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); + updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); + when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + + updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING); + + assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); + assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); + assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); + assertThat(mCachedDevice.isBusy()).isTrue(); + } + + @Test + public void isBusy_memberDeviceIsBonding_returnsBusy() { + mCachedDevice.addMemberDevice(mSubCachedDevice); + updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); + updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); + when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + + when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING); + + assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); + assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); + assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); + assertThat(mCachedDevice.isBusy()).isTrue(); + } + + @Test + public void isBusy_allDevicesAreNotBusy_returnsNotBusy() { + mCachedDevice.addMemberDevice(mSubCachedDevice); + updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); + updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); + when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + + assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); + assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); + assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); + assertThat(mCachedDevice.isBusy()).isFalse(); + } } diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING index 26feaf979b20..cd45b8ea9f61 100644 --- a/packages/SystemUI/TEST_MAPPING +++ b/packages/SystemUI/TEST_MAPPING @@ -1,25 +1,6 @@ { // Looking for unit test presubmit configuration? // This currently lives in ATP config apct/system_ui/unit_test - "presubmit-large": [ - { - "name": "PlatformScenarioTests", - "options": [ - { - "include-filter": "android.platform.test.scenario.sysui" - }, - { - "include-annotation": "android.platform.test.scenario.annotation.Scenario" - }, - { - "exclude-annotation": "org.junit.Ignore" - }, - { - "exclude-annotation": "android.platform.test.annotations.Postsubmit" - } - ] - } - ], "presubmit-sysui": [ { "name": "PlatformScenarioTests", diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 808425435efa..f22e79722e78 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -131,6 +131,9 @@ <!-- For StatusIconContainer to tag its icon views --> <item type="id" name="status_bar_view_state_tag" /> + <!-- Status bar --> + <item type="id" name="status_bar_dot" /> + <!-- Default display cutout on the physical top of screen --> <item type="id" name="display_cutout" /> <item type="id" name="display_cutout_left" /> diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 67b683ec643a..2e13903814a5 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -455,6 +455,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } } + boolean needToUpdateProviderViews = false; final String newUniqueId = mDisplayInfo.uniqueId; if (!Objects.equals(newUniqueId, mDisplayUniqueId)) { mDisplayUniqueId = newUniqueId; @@ -472,6 +473,37 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab setupDecorations(); return; } + + if (mScreenDecorHwcLayer != null) { + updateHwLayerRoundedCornerDrawable(); + updateHwLayerRoundedCornerExistAndSize(); + } + needToUpdateProviderViews = true; + } + + final float newRatio = getPhysicalPixelDisplaySizeRatio(); + if (mRoundedCornerResDelegate.getPhysicalPixelDisplaySizeRatio() != newRatio) { + mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(newRatio); + if (mScreenDecorHwcLayer != null) { + updateHwLayerRoundedCornerExistAndSize(); + } + needToUpdateProviderViews = true; + } + + if (needToUpdateProviderViews) { + updateOverlayProviderViews(null); + } else { + updateOverlayProviderViews(new Integer[] { + mFaceScanningViewId, + R.id.display_cutout, + R.id.display_cutout_left, + R.id.display_cutout_right, + R.id.display_cutout_bottom, + }); + } + + if (mScreenDecorHwcLayer != null) { + mScreenDecorHwcLayer.onDisplayChanged(newUniqueId); } } }; @@ -1037,8 +1069,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod))) { mRotation = newRotation; mDisplayMode = newMod; - mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio( - getPhysicalPixelDisplaySizeRatio()); if (mScreenDecorHwcLayer != null) { mScreenDecorHwcLayer.pendingConfigChange = false; mScreenDecorHwcLayer.updateRotation(mRotation); diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java index 3d5e601f18f5..e342ac2f320d 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java @@ -47,7 +47,8 @@ class IntentCreator { shareIntent.putExtra(Intent.EXTRA_STREAM, clipData.getItemAt(0).getUri()); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { - shareIntent.putExtra(Intent.EXTRA_TEXT, clipData.getItemAt(0).coerceToText(context)); + shareIntent.putExtra( + Intent.EXTRA_TEXT, clipData.getItemAt(0).coerceToText(context).toString()); shareIntent.setType("text/plain"); } Intent chooserIntent = Intent.createChooser(shareIntent, null) diff --git a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt index 8b4aeefb6ed4..a25286438387 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt @@ -78,18 +78,23 @@ class RoundedCornerResDelegate( reloadMeasures() } + private fun reloadAll(newReloadToken: Int) { + if (reloadToken == newReloadToken) { + return + } + reloadToken = newReloadToken + reloadRes() + reloadMeasures() + } + fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) { if (displayUniqueId != newDisplayUniqueId) { displayUniqueId = newDisplayUniqueId newReloadToken ?.let { reloadToken = it } reloadRes() reloadMeasures() - } else if (newReloadToken != null) { - if (reloadToken == newReloadToken) { - return - } - reloadToken = newReloadToken - reloadMeasures() + } else { + newReloadToken?.let { reloadAll(it) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 6f38f4f53b7c..5f96a3b56e27 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -285,6 +285,14 @@ class KeyguardUnlockAnimationController @Inject constructor( var willUnlockWithInWindowLauncherAnimations: Boolean = false /** + * Whether we called [ILauncherUnlockAnimationController.prepareForUnlock], but have not yet + * called [ILauncherUnlockAnimationController.playUnlockAnimation]. This is used exclusively for + * logging purposes to help track down bugs where the Launcher surface is prepared for unlock + * but then never animated. + */ + private var launcherPreparedForUnlock = false + + /** * Whether we decided in [prepareForInWindowLauncherAnimations] that we are able to and want to * play the smartspace shared element animation. If true, * [willUnlockWithInWindowLauncherAnimations] will also always be true since in-window @@ -376,6 +384,20 @@ class KeyguardUnlockAnimationController @Inject constructor( } /** + * Logging helper to log the conditions under which we decide to perform the in-window + * animations. This is used if we prepare to unlock but then somehow decide later to not play + * the animation, which would leave Launcher in a bad state. + */ + private fun logInWindowAnimationConditions() { + Log.wtf(TAG, "canPerformInWindowLauncherAnimations expected all of these to be true: ") + Log.wtf(TAG, " isNexusLauncherUnderneath: ${isNexusLauncherUnderneath()}") + Log.wtf(TAG, " !notificationShadeWindowController.isLaunchingActivity: " + + "${!notificationShadeWindowController.isLaunchingActivity}") + Log.wtf(TAG, " launcherUnlockController != null: ${launcherUnlockController != null}") + Log.wtf(TAG, " !isFoldable(context): ${!isFoldable(context)}") + } + + /** * Called from [KeyguardStateController] to let us know that the keyguard going away state has * changed. */ @@ -384,6 +406,15 @@ class KeyguardUnlockAnimationController @Inject constructor( !statusBarStateController.leaveOpenOnKeyguardHide()) { prepareForInWindowLauncherAnimations() } + + // If the keyguard is no longer going away and we were unlocking with in-window animations, + // make sure that we've left the launcher at 100% unlocked. This is a fail-safe to prevent + // against "tiny launcher" and similar states where the launcher is left in the prepared to + // animate state. + if (!keyguardStateController.isKeyguardGoingAway && + willUnlockWithInWindowLauncherAnimations) { + launcherUnlockController?.setUnlockAmount(1f, true /* forceIfAnimating */) + } } /** @@ -437,6 +468,8 @@ class KeyguardUnlockAnimationController @Inject constructor( lockscreenSmartspaceBounds, /* lockscreenSmartspaceBounds */ selectedPage /* selectedPage */ ) + + launcherPreparedForUnlock = true } catch (e: RemoteException) { Log.e(TAG, "Remote exception in prepareForInWindowUnlockAnimations.", e) } @@ -495,6 +528,8 @@ class KeyguardUnlockAnimationController @Inject constructor( true, UNLOCK_ANIMATION_DURATION_MS + CANNED_UNLOCK_START_DELAY, 0 /* startDelay */) + + launcherPreparedForUnlock = false } else { // Otherwise, we're swiping in an app and should just fade it in. The swipe gesture // will translate it until the end of the swipe gesture. @@ -554,6 +589,12 @@ class KeyguardUnlockAnimationController @Inject constructor( surfaceBehindEntryAnimator.start() } } + + if (launcherPreparedForUnlock && !willUnlockWithInWindowLauncherAnimations) { + Log.wtf(TAG, "Launcher is prepared for unlock, so we should have started the " + + "in-window animation, however we apparently did not.") + logInWindowAnimationConditions() + } } /** @@ -569,6 +610,8 @@ class KeyguardUnlockAnimationController @Inject constructor( LAUNCHER_ICONS_ANIMATION_DURATION_MS /* duration */, CANNED_UNLOCK_START_DELAY /* startDelay */) + launcherPreparedForUnlock = false + // Now that the Launcher surface (with its smartspace positioned identically to ours) is // visible, hide our smartspace. lockscreenSmartspace?.visibility = View.INVISIBLE diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index f8c6a5791839..8368792b8ae3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -149,32 +149,6 @@ class MediaCarouselController @Inject constructor( } } } - - companion object { - private const val SQUISHINESS_SCALE_START = 0.5 - private const val SQUISHINESS_SCALE_FACTOR = 0.5 - private fun getSquishinessScale(squishinessFraction: Float): Double { - return SQUISHINESS_SCALE_START + SQUISHINESS_SCALE_FACTOR * squishinessFraction - } - } - - var squishinessFraction: Float = 1f - set(value) { - if (field == value) { - return - } - field = value - - val scale = getSquishinessScale(field) - for (mediaPlayer in MediaPlayerData.players()) { - mediaPlayer.mediaViewHolder?.let { - it.player.bottom = it.player.top + (scale * it.player.measuredHeight).toInt() - } ?: mediaPlayer.recommendationViewHolder?.let { - it.recommendations.bottom = it.recommendations.top + - (scale * it.recommendations.measuredHeight).toInt() - } - } - } private val configListener = object : ConfigurationController.ConfigurationListener { override fun onDensityOrFontScaleChanged() { // System font changes should only happen when UMO is offscreen or a flicker may occur diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 30947e839f0a..50a10bc0b15a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -541,10 +541,12 @@ public class NavigationBar extends ViewController<NavigationBarView> implements if (!mImeVisible) { // IME not showing, take all touches info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); + return; } if (!mView.isImeRenderingNavButtons()) { // IME showing but not drawing any buttons, take all touches info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); + return; } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index f41b905775e4..18bd6b7b3c32 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -27,7 +27,6 @@ import android.view.View; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaCarouselController; import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.media.MediaHost; import com.android.systemui.media.MediaHostState; @@ -76,14 +75,13 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer, @Named(QS_PANEL) MediaHost mediaHost, QSTileRevealController.Factory qsTileRevealControllerFactory, - DumpManager dumpManager, MediaCarouselController mediaCarouselController, - MetricsLogger metricsLogger, UiEventLogger uiEventLogger, + DumpManager dumpManager, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, BrightnessController.Factory brightnessControllerFactory, BrightnessSliderController.Factory brightnessSliderFactory, FalsingManager falsingManager, StatusBarKeyguardViewManager statusBarKeyguardViewManager) { super(view, qstileHost, qsCustomizerController, usingMediaPlayer, mediaHost, - metricsLogger, uiEventLogger, qsLogger, dumpManager, mediaCarouselController); + metricsLogger, uiEventLogger, qsLogger, dumpManager); mTunerService = tunerService; mQsCustomizerController = qsCustomizerController; mQsTileRevealControllerFactory = qsTileRevealControllerFactory; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index a5c60a417a05..ded466a0cb25 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -32,7 +32,6 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dumpable; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaCarouselController; import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; @@ -71,7 +70,6 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr private final UiEventLogger mUiEventLogger; private final QSLogger mQSLogger; private final DumpManager mDumpManager; - private final MediaCarouselController mMediaCarouselController; protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); protected boolean mShouldUseSplitNotificationShade; @@ -133,8 +131,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, - DumpManager dumpManager, - MediaCarouselController mediaCarouselController + DumpManager dumpManager ) { super(view); mHost = host; @@ -147,7 +144,6 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr mDumpManager = dumpManager; mShouldUseSplitNotificationShade = LargeScreenUtils.shouldUseSplitNotificationShade(getResources()); - mMediaCarouselController = mediaCarouselController; } @Override @@ -165,7 +161,6 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr public void setSquishinessFraction(float squishinessFraction) { mView.setSquishinessFraction(squishinessFraction); - mMediaCarouselController.setSquishinessFraction(squishinessFraction); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java index bd75c75faa00..ae6ed2008a77 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java @@ -444,7 +444,7 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { mShouldUseSettingsButton.set(false); mBgHandler.post(() -> { String settingsButtonText = getSettingsButton(); - final View dialogView = createDialogView(); + final View dialogView = createDialogView(quickSettingsContext); mMainHandler.post(() -> { mDialog = new SystemUIDialog(quickSettingsContext, 0); mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -469,14 +469,14 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { } @VisibleForTesting - View createDialogView() { + View createDialogView(Context quickSettingsContext) { if (mSecurityController.isParentalControlsEnabled()) { return createParentalControlsDialogView(); } - return createOrganizationDialogView(); + return createOrganizationDialogView(quickSettingsContext); } - private View createOrganizationDialogView() { + private View createOrganizationDialogView(Context quickSettingsContext) { final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); final CharSequence deviceOwnerOrganization = @@ -487,7 +487,7 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { final String vpnName = mSecurityController.getPrimaryVpnName(); final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); - View dialogView = LayoutInflater.from(mContext) + View dialogView = LayoutInflater.from(quickSettingsContext) .inflate(R.layout.quick_settings_footer_dialog, null, false); // device management section diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java index 7ce0ad04bb75..9739974256f6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java @@ -26,7 +26,6 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaCarouselController; import com.android.systemui.media.MediaHierarchyManager; import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.QSTile; @@ -56,10 +55,10 @@ public class QuickQSPanelController extends QSPanelControllerBase<QuickQSPanel> @Named(QS_USING_COLLAPSED_LANDSCAPE_MEDIA) Provider<Boolean> usingCollapsedLandscapeMediaProvider, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, - DumpManager dumpManager, MediaCarouselController mediaCarouselController + DumpManager dumpManager ) { super(view, qsTileHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger, - uiEventLogger, qsLogger, dumpManager, mediaCarouselController); + uiEventLogger, qsLogger, dumpManager); mUsingCollapsedLandscapeMediaProvider = usingCollapsedLandscapeMediaProvider; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java index b5859616f392..ccaab1adaf26 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java @@ -30,6 +30,7 @@ import com.android.systemui.qs.carrier.QSCarrierGroupController; import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider; import com.android.systemui.statusbar.phone.StatusBarIconController; +import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.phone.StatusIconContainer; import com.android.systemui.statusbar.policy.Clock; import com.android.systemui.statusbar.policy.VariableDateViewController; @@ -104,7 +105,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader mView.requireViewById(R.id.date_clock) ); - mIconManager = tintedIconManagerFactory.create(mIconContainer); + mIconManager = tintedIconManagerFactory.create(mIconContainer, StatusBarLocation.QS); mDemoModeReceiver = new ClockDemoModeReceiver(mClockView); mColorExtractor = colorExtractor; mOnColorsChangedListener = (extractor, which) -> { diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt index fe40d4cbe23a..d3ed47407b9d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt @@ -48,6 +48,7 @@ import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QQS import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.phone.StatusBarIconController +import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_BATTERY_CONTROLLER @@ -261,7 +262,7 @@ class LargeScreenShadeHeaderController @Inject constructor( batteryMeterViewController.ignoreTunerUpdates() batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE) - iconManager = tintedIconManagerFactory.create(iconContainer) + iconManager = tintedIconManagerFactory.create(iconContainer, StatusBarLocation.QS) iconManager.setTint( Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary) ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index 827d0d0f8444..c35c5c522798 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -22,6 +22,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; +import android.annotation.IntDef; import android.app.ActivityManager; import android.app.Notification; import android.content.Context; @@ -59,6 +60,8 @@ import com.android.systemui.statusbar.notification.NotificationIconDozeHelper; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.util.drawable.DrawableSize; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; @@ -86,6 +89,10 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi public static final int STATE_DOT = 1; public static final int STATE_HIDDEN = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_ICON, STATE_DOT, STATE_HIDDEN}) + public @interface VisibleState { } + private static final String TAG = "StatusBarIconView"; private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT = new FloatProperty<StatusBarIconView>("iconAppearAmount") { @@ -133,6 +140,7 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi private final Paint mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private float mDotRadius; private int mStaticDotRadius; + @StatusBarIconView.VisibleState private int mVisibleState = STATE_ICON; private float mIconAppearAmount = 1.0f; private ObjectAnimator mIconAppearAnimator; @@ -746,11 +754,12 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi } @Override - public void setVisibleState(int state) { + public void setVisibleState(@StatusBarIconView.VisibleState int state) { setVisibleState(state, true /* animate */, null /* endRunnable */); } - public void setVisibleState(int state, boolean animate) { + @Override + public void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate) { setVisibleState(state, animate, null); } @@ -862,6 +871,7 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi return mIconAppearAmount; } + @StatusBarIconView.VisibleState public int getVisibleState() { return mVisibleState; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java index 25c6dce96b5c..48c6e273bbb4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java @@ -59,7 +59,8 @@ public class StatusBarMobileView extends FrameLayout implements DarkReceiver, private ImageView mOut; private ImageView mMobile, mMobileType, mMobileRoaming; private View mMobileRoamingSpace; - private int mVisibleState = -1; + @StatusBarIconView.VisibleState + private int mVisibleState = STATE_HIDDEN; private DualToneHandler mDualToneHandler; private boolean mForceHidden; @@ -271,7 +272,7 @@ public class StatusBarMobileView extends FrameLayout implements DarkReceiver, } @Override - public void setVisibleState(int state, boolean animate) { + public void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate) { if (state == mVisibleState) { return; } @@ -312,6 +313,7 @@ public class StatusBarMobileView extends FrameLayout implements DarkReceiver, } @Override + @StatusBarIconView.VisibleState public int getVisibleState() { return mVisibleState; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java index 5aee62e3e89f..f3e74d92fc8a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java @@ -55,7 +55,8 @@ public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkRece private View mAirplaneSpacer; private WifiIconState mState; private String mSlot; - private int mVisibleState = -1; + @StatusBarIconView.VisibleState + private int mVisibleState = STATE_HIDDEN; public static StatusBarWifiView fromContext(Context context, String slot) { LayoutInflater inflater = LayoutInflater.from(context); @@ -107,7 +108,7 @@ public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkRece } @Override - public void setVisibleState(int state, boolean animate) { + public void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate) { if (state == mVisibleState) { return; } @@ -131,6 +132,7 @@ public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkRece } @Override + @StatusBarIconView.VisibleState public int getVisibleState() { return mVisibleState; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusIconDisplayable.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusIconDisplayable.java index d541fae4ed33..1196211bd671 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusIconDisplayable.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusIconDisplayable.java @@ -22,14 +22,32 @@ public interface StatusIconDisplayable extends DarkReceiver { String getSlot(); void setStaticDrawableColor(int color); void setDecorColor(int color); - default void setVisibleState(int state) { + + /** Sets the visible state that this displayable should be. */ + default void setVisibleState(@StatusBarIconView.VisibleState int state) { setVisibleState(state, false); } - void setVisibleState(int state, boolean animate); + + /** + * Sets the visible state that this displayable should be, and whether the change should + * animate. + */ + void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate); + + /** Returns the current visible state of this displayable. */ + @StatusBarIconView.VisibleState int getVisibleState(); + + /** + * Returns true if this icon should be visible if there's space, and false otherwise. + * + * Note that this doesn't necessarily mean it *will* be visible. It's possible that there are + * more icons than space, in which case this icon might just show a dot or might be completely + * hidden. {@link #getVisibleState} will return the icon's actual visible status. + */ boolean isIconVisible(); + default boolean isIconBlocked() { return false; } } - diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index ce2c9c244696..0026b71a5304 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -352,8 +352,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); mDisableStateTracker.startTracking(mCommandQueue, mView.getDisplay().getDisplayId()); if (mTintedIconManager == null) { - mTintedIconManager = - mTintedIconManagerFactory.create(mView.findViewById(R.id.statusIcons)); + mTintedIconManager = mTintedIconManagerFactory.create( + mView.findViewById(R.id.statusIcons), StatusBarLocation.KEYGUARD); mTintedIconManager.setBlockList(getBlockedIcons()); mStatusBarIconController.addIconGroup(mTintedIconManager); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index bd99713e3a69..d6d021ff2819 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -56,7 +56,6 @@ import java.util.ArrayList; import java.util.List; import javax.inject.Inject; -import javax.inject.Provider; public interface StatusBarIconController { @@ -139,13 +138,15 @@ public interface StatusBarIconController { public DarkIconManager( LinearLayout linearLayout, + StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, - Provider<WifiViewModel> wifiViewModelProvider, + WifiViewModel wifiViewModel, MobileContextProvider mobileContextProvider, DarkIconDispatcher darkIconDispatcher) { super(linearLayout, + location, statusBarPipelineFlags, - wifiViewModelProvider, + wifiViewModel, mobileContextProvider); mIconHPadding = mContext.getResources().getDimensionPixelSize( R.dimen.status_bar_icon_padding); @@ -204,27 +205,28 @@ public interface StatusBarIconController { @SysUISingleton public static class Factory { private final StatusBarPipelineFlags mStatusBarPipelineFlags; - private final Provider<WifiViewModel> mWifiViewModelProvider; + private final WifiViewModel mWifiViewModel; private final MobileContextProvider mMobileContextProvider; private final DarkIconDispatcher mDarkIconDispatcher; @Inject public Factory( StatusBarPipelineFlags statusBarPipelineFlags, - Provider<WifiViewModel> wifiViewModelProvider, + WifiViewModel wifiViewModel, MobileContextProvider mobileContextProvider, DarkIconDispatcher darkIconDispatcher) { mStatusBarPipelineFlags = statusBarPipelineFlags; - mWifiViewModelProvider = wifiViewModelProvider; + mWifiViewModel = wifiViewModel; mMobileContextProvider = mobileContextProvider; mDarkIconDispatcher = darkIconDispatcher; } - public DarkIconManager create(LinearLayout group) { + public DarkIconManager create(LinearLayout group, StatusBarLocation location) { return new DarkIconManager( group, + location, mStatusBarPipelineFlags, - mWifiViewModelProvider, + mWifiViewModel, mMobileContextProvider, mDarkIconDispatcher); } @@ -239,12 +241,14 @@ public interface StatusBarIconController { public TintedIconManager( ViewGroup group, + StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, - Provider<WifiViewModel> wifiViewModelProvider, + WifiViewModel wifiViewModel, MobileContextProvider mobileContextProvider) { super(group, + location, statusBarPipelineFlags, - wifiViewModelProvider, + wifiViewModel, mobileContextProvider); } @@ -278,24 +282,25 @@ public interface StatusBarIconController { @SysUISingleton public static class Factory { private final StatusBarPipelineFlags mStatusBarPipelineFlags; - private final Provider<WifiViewModel> mWifiViewModelProvider; + private final WifiViewModel mWifiViewModel; private final MobileContextProvider mMobileContextProvider; @Inject public Factory( StatusBarPipelineFlags statusBarPipelineFlags, - Provider<WifiViewModel> wifiViewModelProvider, + WifiViewModel wifiViewModel, MobileContextProvider mobileContextProvider) { mStatusBarPipelineFlags = statusBarPipelineFlags; - mWifiViewModelProvider = wifiViewModelProvider; + mWifiViewModel = wifiViewModel; mMobileContextProvider = mobileContextProvider; } - public TintedIconManager create(ViewGroup group) { + public TintedIconManager create(ViewGroup group, StatusBarLocation location) { return new TintedIconManager( group, + location, mStatusBarPipelineFlags, - mWifiViewModelProvider, + mWifiViewModel, mMobileContextProvider); } } @@ -306,8 +311,9 @@ public interface StatusBarIconController { */ class IconManager implements DemoModeCommandReceiver { protected final ViewGroup mGroup; + private final StatusBarLocation mLocation; private final StatusBarPipelineFlags mStatusBarPipelineFlags; - private final Provider<WifiViewModel> mWifiViewModelProvider; + private final WifiViewModel mWifiViewModel; private final MobileContextProvider mMobileContextProvider; protected final Context mContext; protected final int mIconSize; @@ -324,12 +330,14 @@ public interface StatusBarIconController { public IconManager( ViewGroup group, + StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, - Provider<WifiViewModel> wifiViewModelProvider, + WifiViewModel wifiViewModel, MobileContextProvider mobileContextProvider) { mGroup = group; + mLocation = location; mStatusBarPipelineFlags = statusBarPipelineFlags; - mWifiViewModelProvider = wifiViewModelProvider; + mWifiViewModel = wifiViewModel; mMobileContextProvider = mobileContextProvider; mContext = group.getContext(); mIconSize = mContext.getResources().getDimensionPixelSize( @@ -446,7 +454,7 @@ public interface StatusBarIconController { private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) { return ModernStatusBarWifiView.constructAndBind( - mContext, slot, mWifiViewModelProvider.get()); + mContext, slot, mWifiViewModel, mLocation); } private StatusBarMobileView onCreateStatusBarMobileView(int subId, String slot) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index e61794b0243e..9d5392af3127 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -730,7 +730,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private void setDozing(boolean dozing) { if (mDozing != dozing) { mDozing = dozing; - reset(true /* hideBouncerWhenShowing */); + if (dozing || mBouncer.needsFullscreenBouncer() || mOccluded) { + reset(dozing /* hideBouncerWhenShowing */); + } updateStates(); if (!dozing) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt new file mode 100644 index 000000000000..5ace22695ec3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +/** An enumeration of the different locations that host a status bar. */ +enum class StatusBarLocation { + /** Home screen or in-app. */ + HOME, + /** Keyguard (aka lockscreen). */ + KEYGUARD, + /** Quick settings (inside the shade). */ + QS, +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index ce04fb599963..e1215ee95238 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -68,6 +68,7 @@ import com.android.systemui.statusbar.phone.PhoneStatusBarView; import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager; +import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.phone.StatusBarLocationPublisher; import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent; import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent.Startable; @@ -250,7 +251,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mStatusBar.restoreHierarchyState( savedInstanceState.getSparseParcelableArray(EXTRA_PANEL_STATE)); } - mDarkIconManager = mDarkIconManagerFactory.create(view.findViewById(R.id.statusIcons)); + mDarkIconManager = mDarkIconManagerFactory.create( + view.findViewById(R.id.statusIcons), StatusBarLocation.HOME); mDarkIconManager.setShouldLog(true); updateBlockedIcons(); mStatusBarIconController.addIconGroup(mDarkIconManager); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt new file mode 100644 index 000000000000..118b94c7aa83 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.shared + +import android.telephony.TelephonyManager +import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG +import java.io.PrintWriter +import javax.inject.Inject + +/** + * An object storing constants that are used for calculating connectivity icons. + * + * Stored in a class for logging purposes. + */ +@SysUISingleton +class ConnectivityConstants +@Inject +constructor(dumpManager: DumpManager, telephonyManager: TelephonyManager) : Dumpable { + init { + dumpManager.registerDumpable("$SB_LOGGING_TAG:ConnectivityConstants", this) + } + + /** True if this device has the capability for data connections and false otherwise. */ + val hasDataCapabilities = telephonyManager.isDataCapable + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.apply { println("hasDataCapabilities=$hasDataCapabilities") } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt index 88d8a86d39f2..dbb1aa54d8ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt @@ -33,6 +33,20 @@ class ConnectivityPipelineLogger @Inject constructor( ) { /** * Logs a change in one of the **raw inputs** to the connectivity pipeline. + * + * Use this method for inputs that don't have any extra information besides their callback name. + */ + fun logInputChange(callbackName: String) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { str1 = callbackName }, + { "Input: $str1" } + ) + } + + /** + * Logs a change in one of the **raw inputs** to the connectivity pipeline. */ fun logInputChange(callbackName: String, changeInfo: String?) { buffer.log( @@ -128,12 +142,36 @@ class ConnectivityPipelineLogger @Inject constructor( const val SB_LOGGING_TAG = "SbConnectivity" /** + * Log a change in one of the **inputs** to the connectivity pipeline. + */ + fun Flow<Unit>.logInputChange( + logger: ConnectivityPipelineLogger, + inputParamName: String, + ): Flow<Unit> { + return this.onEach { logger.logInputChange(inputParamName) } + } + + /** + * Log a change in one of the **inputs** to the connectivity pipeline. + * + * @param prettyPrint an optional function to transform the value into a readable string. + * [toString] is used if no custom function is provided. + */ + fun <T> Flow<T>.logInputChange( + logger: ConnectivityPipelineLogger, + inputParamName: String, + prettyPrint: (T) -> String = { it.toString() } + ): Flow<T> { + return this.onEach {logger.logInputChange(inputParamName, prettyPrint(it)) } + } + + /** * Log a change in one of the **outputs** to the connectivity pipeline. * * @param prettyPrint an optional function to transform the value into a readable string. * [toString] is used if no custom function is provided. */ - fun <T : Any> Flow<T>.logOutputChange( + fun <T> Flow<T>.logOutputChange( logger: ConnectivityPipelineLogger, outputParamName: String, prettyPrint: (T) -> String = { it.toString() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt index 103f3fc21f91..681cf7254ae7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository import android.annotation.SuppressLint +import android.content.IntentFilter import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities @@ -30,51 +31,87 @@ import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback import android.util.Log import com.android.settingslib.Utils +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.stateIn -/** - * Provides data related to the wifi state. - */ +/** Provides data related to the wifi state. */ interface WifiRepository { - /** - * Observable for the current wifi network. - */ - val wifiNetwork: Flow<WifiNetworkModel> - - /** - * Observable for the current wifi network activity. - */ - val wifiActivity: Flow<WifiActivityModel> + /** Observable for the current wifi enabled status. */ + val isWifiEnabled: StateFlow<Boolean> + + /** Observable for the current wifi network. */ + val wifiNetwork: StateFlow<WifiNetworkModel> + + /** Observable for the current wifi network activity. */ + val wifiActivity: StateFlow<WifiActivityModel> } /** Real implementation of [WifiRepository]. */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton @SuppressLint("MissingPermission") class WifiRepositoryImpl @Inject constructor( + broadcastDispatcher: BroadcastDispatcher, connectivityManager: ConnectivityManager, logger: ConnectivityPipelineLogger, @Main mainExecutor: Executor, @Application scope: CoroutineScope, wifiManager: WifiManager?, ) : WifiRepository { - override val wifiNetwork: Flow<WifiNetworkModel> = conflatedCallbackFlow { + + private val wifiStateChangeEvents: Flow<Unit> = broadcastDispatcher.broadcastFlow( + IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION) + ) + .logInputChange(logger, "WIFI_STATE_CHANGED_ACTION intent") + + private val wifiNetworkChangeEvents: MutableSharedFlow<Unit> = + MutableSharedFlow(extraBufferCapacity = 1) + + override val isWifiEnabled: StateFlow<Boolean> = + if (wifiManager == null) { + MutableStateFlow(false).asStateFlow() + } else { + // Because [WifiManager] doesn't expose a wifi enabled change listener, we do it + // internally by fetching [WifiManager.isWifiEnabled] whenever we think the state may + // have changed. + merge(wifiNetworkChangeEvents, wifiStateChangeEvents) + .mapLatest { wifiManager.isWifiEnabled } + .distinctUntilChanged() + .logOutputChange(logger, "enabled") + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(), + initialValue = wifiManager.isWifiEnabled + ) + } + + override val wifiNetwork: StateFlow<WifiNetworkModel> = conflatedCallbackFlow { var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { @@ -84,6 +121,8 @@ class WifiRepositoryImpl @Inject constructor( ) { logger.logOnCapabilitiesChanged(network, networkCapabilities) + wifiNetworkChangeEvents.tryEmit(Unit) + val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) if (wifiInfo?.isPrimary == true) { val wifiNetworkModel = createWifiNetworkModel( @@ -104,6 +143,9 @@ class WifiRepositoryImpl @Inject constructor( override fun onLost(network: Network) { logger.logOnLost(network) + + wifiNetworkChangeEvents.tryEmit(Unit) + val wifi = currentWifi if (wifi is WifiNetworkModel.Active && wifi.networkId == network.getNetId()) { val newNetworkModel = WifiNetworkModel.Inactive @@ -132,7 +174,7 @@ class WifiRepositoryImpl @Inject constructor( initialValue = WIFI_NETWORK_DEFAULT ) - override val wifiActivity: Flow<WifiActivityModel> = + override val wifiActivity: StateFlow<WifiActivityModel> = if (wifiManager == null) { Log.w(SB_LOGGING_TAG, "Null WifiManager; skipping activity callback") flowOf(ACTIVITY_DEFAULT) @@ -142,13 +184,15 @@ class WifiRepositoryImpl @Inject constructor( logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state)) trySend(trafficStateToWifiActivityModel(state)) } - - trySend(ACTIVITY_DEFAULT) wifiManager.registerTrafficStateCallback(mainExecutor, callback) - awaitClose { wifiManager.unregisterTrafficStateCallback(callback) } } } + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + initialValue = ACTIVITY_DEFAULT + ) companion object { val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt index 952525d243f9..04b17ed2924a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt @@ -22,9 +22,10 @@ import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlo import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map /** @@ -38,7 +39,11 @@ class WifiInteractor @Inject constructor( connectivityRepository: ConnectivityRepository, wifiRepository: WifiRepository, ) { - private val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info -> + /** + * The SSID (service set identifier) of the wifi network. Null if we don't have a network, or + * have a network but no valid SSID. + */ + val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info -> when (info) { is WifiNetworkModel.Inactive -> null is WifiNetworkModel.CarrierMerged -> null @@ -51,17 +56,17 @@ class WifiInteractor @Inject constructor( } } + /** Our current enabled status. */ + val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled + /** Our current wifi network. See [WifiNetworkModel]. */ val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork + /** Our current wifi activity. See [WifiActivityModel]. */ + val activity: StateFlow<WifiActivityModel> = wifiRepository.wifiActivity + /** True if we're configured to force-hide the wifi icon and false otherwise. */ val isForceHidden: Flow<Boolean> = connectivityRepository.forceHiddenSlots.map { it.contains(ConnectivitySlot.WIFI) } - - /** True if our wifi network has activity in (download), and false otherwise. */ - val hasActivityIn: Flow<Boolean> = - combine(wifiRepository.wifiActivity, ssid) { activity, ssid -> - activity.hasActivityIn && ssid != null - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt index a19d1bdd8e62..0eb4b0de9f6b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt @@ -41,9 +41,14 @@ class WifiConstants @Inject constructor( /** True if we should show the activityIn/activityOut icons and false otherwise. */ val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity) + /** True if we should always show the wifi icon when wifi is enabled and false otherwise. */ + val alwaysShowIconIfEnabled = + context.resources.getBoolean(R.bool.config_showWifiIndicatorWhenEnabled) + override fun dump(pw: PrintWriter, args: Array<out String>) { pw.apply { println("shouldShowActivityConfig=$shouldShowActivityConfig") + println("alwaysShowIconIfEnabled=$alwaysShowIconIfEnabled") } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt index 44c04968041e..574610605b4e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.systemui.statusbar.pipeline.wifi.data.model +package com.android.systemui.statusbar.pipeline.wifi.shared.model -/** - * Provides information on the current wifi activity. - */ +/** Provides information on the current wifi activity. */ data class WifiActivityModel( /** True if the wifi has activity in (download). */ val hasActivityIn: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt index 4fad3274d12f..273be63eb8a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt @@ -26,8 +26,15 @@ import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.R import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT +import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -41,40 +48,111 @@ import kotlinx.coroutines.launch */ @OptIn(InternalCoroutinesApi::class) object WifiViewBinder { - /** Binds the view to the view-model, continuing to update the former based on the latter. */ + + /** + * Defines interface for an object that acts as the binding between the view and its view-model. + * + * Users of the [WifiViewBinder] class should use this to control the binder after it is bound. + */ + interface Binding { + /** Returns true if the wifi icon should be visible and false otherwise. */ + fun getShouldIconBeVisible(): Boolean + + /** Notifies that the visibility state has changed. */ + fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) + } + + /** + * Binds the view to the appropriate view-model based on the given location. The view will + * continue to be updated following updates from the view-model. + */ @JvmStatic fun bind( view: ViewGroup, - viewModel: WifiViewModel, - ) { + wifiViewModel: WifiViewModel, + location: StatusBarLocation, + ): Binding { + return when (location) { + StatusBarLocation.HOME -> bind(view, wifiViewModel.home) + StatusBarLocation.KEYGUARD -> bind(view, wifiViewModel.keyguard) + StatusBarLocation.QS -> bind(view, wifiViewModel.qs) + } + } + + /** Binds the view to the view-model, continuing to update the former based on the latter. */ + @JvmStatic + private fun bind( + view: ViewGroup, + viewModel: LocationBasedWifiViewModel, + ): Binding { + val groupView = view.requireViewById<ViewGroup>(R.id.wifi_group) val iconView = view.requireViewById<ImageView>(R.id.wifi_signal) + val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot) + val activityInView = view.requireViewById<ImageView>(R.id.wifi_in) + val activityOutView = view.requireViewById<ImageView>(R.id.wifi_out) + val activityContainerView = view.requireViewById<View>(R.id.inout_container) view.isVisible = true iconView.isVisible = true + // TODO(b/238425913): We should log this visibility state. + @StatusBarIconView.VisibleState + val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN) + view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { - viewModel.wifiIcon.distinctUntilChanged().collect { wifiIcon -> - // TODO(b/238425913): Right now, if !isVisible, there's just an empty space - // where the wifi icon would be. We need to pipe isVisible through to - // [ModernStatusBarWifiView.isIconVisible], which is what actually makes - // the view GONE. + visibilityState.collect { visibilityState -> + groupView.isVisible = visibilityState == STATE_ICON + dotView.isVisible = visibilityState == STATE_DOT + } + } + + launch { + viewModel.wifiIcon.collect { wifiIcon -> view.isVisible = wifiIcon != null - wifiIcon?.let { - IconViewBinder.bind(wifiIcon, iconView) - } + wifiIcon?.let { IconViewBinder.bind(wifiIcon, iconView) } } } launch { viewModel.tint.collect { tint -> - iconView.imageTintList = ColorStateList.valueOf(tint) + val tintList = ColorStateList.valueOf(tint) + iconView.imageTintList = tintList + activityInView.imageTintList = tintList + activityOutView.imageTintList = tintList + dotView.setDecorColor(tint) + } + } + + launch { + viewModel.isActivityInViewVisible.distinctUntilChanged().collect { visible -> + activityInView.isVisible = visible + } + } + + launch { + viewModel.isActivityOutViewVisible.distinctUntilChanged().collect { visible -> + activityOutView.isVisible = visible + } + } + + launch { + viewModel.isActivityContainerVisible.distinctUntilChanged().collect { visible -> + activityContainerView.isVisible = visible } } } } - // TODO(b/238425913): Hook up to [viewModel] to render actual changes to the wifi icon. + return object : Binding { + override fun getShouldIconBeVisible(): Boolean { + return viewModel.wifiIcon.value != null + } + + override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) { + visibilityState.value = state + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt index c14a897fffab..6c616ac7c3b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt @@ -19,10 +19,14 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.view import android.content.Context import android.graphics.Rect import android.util.AttributeSet +import android.view.Gravity import android.view.LayoutInflater import com.android.systemui.R import com.android.systemui.statusbar.BaseStatusBarWifiView -import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT +import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel @@ -36,6 +40,17 @@ class ModernStatusBarWifiView( ) : BaseStatusBarWifiView(context, attrs) { private lateinit var slot: String + private lateinit var binding: WifiViewBinder.Binding + + @StatusBarIconView.VisibleState + private var iconVisibleState: Int = STATE_HIDDEN + set(value) { + if (field == value) { + return + } + field = value + binding.onVisibilityStateChanged(value) + } override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) { // TODO(b/238425913) @@ -51,42 +66,64 @@ class ModernStatusBarWifiView( // TODO(b/238425913) } - override fun setVisibleState(state: Int, animate: Boolean) { - // TODO(b/238425913) + override fun setVisibleState(@StatusBarIconView.VisibleState state: Int, animate: Boolean) { + iconVisibleState = state } + @StatusBarIconView.VisibleState override fun getVisibleState(): Int { - // TODO(b/238425913) - return STATE_ICON + return iconVisibleState } override fun isIconVisible(): Boolean { - // TODO(b/238425913) - return true + return binding.getShouldIconBeVisible() } - /** Set the slot name for this view. */ - private fun setSlot(slotName: String) { - this.slot = slotName + private fun initView( + slotName: String, + wifiViewModel: WifiViewModel, + location: StatusBarLocation, + ) { + slot = slotName + initDotView() + binding = WifiViewBinder.bind(this, wifiViewModel, location) + } + + // Mostly duplicated from [com.android.systemui.statusbar.StatusBarWifiView]. + private fun initDotView() { + // TODO(b/238425913): Could we just have this dot view be part of + // R.layout.new_status_bar_wifi_group with a dot drawable so we don't need to inflate it + // manually? Would that not work with animations? + val dotView = StatusBarIconView(mContext, slot, null).also { + it.id = R.id.status_bar_dot + // Hard-code this view to always be in the DOT state so that whenever it's visible it + // will show a dot + it.visibleState = STATE_DOT + } + + val width = mContext.resources.getDimensionPixelSize(R.dimen.status_bar_icon_size) + val lp = LayoutParams(width, width) + lp.gravity = Gravity.CENTER_VERTICAL or Gravity.START + addView(dotView, lp) } companion object { /** - * Inflates a new instance of [ModernStatusBarWifiView], binds it to [viewModel], and + * Inflates a new instance of [ModernStatusBarWifiView], binds it to a view model, and * returns it. */ @JvmStatic fun constructAndBind( context: Context, slot: String, - viewModel: WifiViewModel, + wifiViewModel: WifiViewModel, + location: StatusBarLocation, ): ModernStatusBarWifiView { return ( LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null) as ModernStatusBarWifiView ).also { - it.setSlot(slot) - WifiViewBinder.bind(it, viewModel) + it.initView(slot, wifiViewModel, location) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt new file mode 100644 index 000000000000..40f948f9ee6c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel + +import android.graphics.Color +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** + * A view model for the wifi icon shown on the "home" page (aka, when the device is unlocked and not + * showing the shade, so the user is on the home-screen, or in an app). + */ +class HomeWifiViewModel( + statusBarPipelineFlags: StatusBarPipelineFlags, + wifiIcon: StateFlow<Icon.Resource?>, + isActivityInViewVisible: Flow<Boolean>, + isActivityOutViewVisible: Flow<Boolean>, + isActivityContainerVisible: Flow<Boolean>, +) : + LocationBasedWifiViewModel( + statusBarPipelineFlags, + debugTint = Color.CYAN, + wifiIcon, + isActivityInViewVisible, + isActivityOutViewVisible, + isActivityContainerVisible, + ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt new file mode 100644 index 000000000000..9642ac42972e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel + +import android.graphics.Color +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** A view model for the wifi icon shown on keyguard (lockscreen). */ +class KeyguardWifiViewModel( + statusBarPipelineFlags: StatusBarPipelineFlags, + wifiIcon: StateFlow<Icon.Resource?>, + isActivityInViewVisible: Flow<Boolean>, + isActivityOutViewVisible: Flow<Boolean>, + isActivityContainerVisible: Flow<Boolean>, +) : + LocationBasedWifiViewModel( + statusBarPipelineFlags, + debugTint = Color.MAGENTA, + wifiIcon, + isActivityInViewVisible, + isActivityOutViewVisible, + isActivityContainerVisible, + ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt new file mode 100644 index 000000000000..e23f8c7e97e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel + +import android.graphics.Color +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf + +/** + * A view model for a wifi icon in a specific location. This allows us to control parameters that + * are location-specific (for example, different tints of the icon in different locations). + * + * Must be subclassed for each distinct location. + */ +abstract class LocationBasedWifiViewModel( + statusBarPipelineFlags: StatusBarPipelineFlags, + debugTint: Int, + + /** The wifi icon that should be displayed. Null if we shouldn't display any icon. */ + val wifiIcon: StateFlow<Icon.Resource?>, + + /** True if the activity in view should be visible. */ + val isActivityInViewVisible: Flow<Boolean>, + + /** True if the activity out view should be visible. */ + val isActivityOutViewVisible: Flow<Boolean>, + + /** True if the activity container view should be visible. */ + val isActivityContainerVisible: Flow<Boolean>, +) { + /** The color that should be used to tint the icon. */ + val tint: Flow<Int> = + flowOf( + if (statusBarPipelineFlags.useNewPipelineDebugColoring()) { + debugTint + } else { + DEFAULT_TINT + } + ) + + companion object { + /** + * A default icon tint. + * + * TODO(b/238425913): The tint is actually controlled by + * [com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager]. We + * should use that logic instead of white as a default. + */ + private const val DEFAULT_TINT = Color.WHITE + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt new file mode 100644 index 000000000000..0ddf90e21872 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel + +import android.graphics.Color +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** A view model for the wifi icon shown in quick settings (when the shade is pulled down). */ +class QsWifiViewModel( + statusBarPipelineFlags: StatusBarPipelineFlags, + wifiIcon: StateFlow<Icon.Resource?>, + isActivityInViewVisible: Flow<Boolean>, + isActivityOutViewVisible: Flow<Boolean>, + isActivityContainerVisible: Flow<Boolean>, +) : + LocationBasedWifiViewModel( + statusBarPipelineFlags, + debugTint = Color.GREEN, + wifiIcon, + isActivityInViewVisible, + isActivityOutViewVisible, + isActivityContainerVisible, + ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index 3c243ac90831..ebbd77b72014 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import android.content.Context -import android.graphics.Color import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting @@ -26,107 +25,185 @@ import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTI import com.android.systemui.R import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn /** * Models the UI state for the status bar wifi icon. + * + * This class exposes three view models, one per status bar location: + * - [home] + * - [keyguard] + * - [qs] + * In order to get the UI state for the wifi icon, you must use one of those view models (whichever + * is correct for your location). + * + * Internally, this class maintains the current state of the wifi icon and notifies those three + * view models of any changes. */ -class WifiViewModel @Inject constructor( - statusBarPipelineFlags: StatusBarPipelineFlags, - private val constants: WifiConstants, +@SysUISingleton +class WifiViewModel +@Inject +constructor( + connectivityConstants: ConnectivityConstants, private val context: Context, - private val logger: ConnectivityPipelineLogger, - private val interactor: WifiInteractor, + logger: ConnectivityPipelineLogger, + interactor: WifiInteractor, + @Application private val scope: CoroutineScope, + statusBarPipelineFlags: StatusBarPipelineFlags, + wifiConstants: WifiConstants, ) { /** - * The drawable resource ID to use for the wifi icon. Null if we shouldn't display any icon. + * Returns the drawable resource ID to use for the wifi icon based on the given network. + * Null if we can't compute the icon. */ @DrawableRes - private val iconResId: Flow<Int?> = interactor.wifiNetwork.map { - when (it) { + private fun WifiNetworkModel.iconResId(): Int? { + return when (this) { is WifiNetworkModel.CarrierMerged -> null is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK is WifiNetworkModel.Active -> when { - it.level == null -> null - it.isValidated -> WIFI_FULL_ICONS[it.level] - else -> WIFI_NO_INTERNET_ICONS[it.level] + this.level == null -> null + this.isValidated -> WIFI_FULL_ICONS[this.level] + else -> WIFI_NO_INTERNET_ICONS[this.level] } } } - /** The content description for the wifi icon. */ - private val contentDescription: Flow<ContentDescription?> = interactor.wifiNetwork.map { - when (it) { + /** + * Returns the content description for the wifi icon based on the given network. + * Null if we can't compute the content description. + */ + private fun WifiNetworkModel.contentDescription(): ContentDescription? { + return when (this) { is WifiNetworkModel.CarrierMerged -> null is WifiNetworkModel.Inactive -> ContentDescription.Loaded( "${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}" ) is WifiNetworkModel.Active -> - when (it.level) { + when (this.level) { null -> null else -> { - val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[it.level]) + val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level]) when { - it.isValidated -> ContentDescription.Loaded(levelDesc) - else -> ContentDescription.Loaded( - "$levelDesc,${context.getString(NO_INTERNET)}" - ) + this.isValidated -> ContentDescription.Loaded(levelDesc) + else -> + ContentDescription.Loaded( + "$levelDesc,${context.getString(NO_INTERNET)}" + ) } } } } } - /** - * The wifi icon that should be displayed. Null if we shouldn't display any icon. - */ - val wifiIcon: Flow<Icon?> = combine( + /** The wifi icon that should be displayed. Null if we shouldn't display any icon. */ + private val wifiIcon: StateFlow<Icon.Resource?> = + combine( + interactor.isEnabled, interactor.isForceHidden, - iconResId, - contentDescription, - ) { isForceHidden, iconResId, contentDescription -> - when { - isForceHidden || - iconResId == null || - iconResId <= 0 -> null - else -> Icon.Resource(iconResId, contentDescription) + interactor.wifiNetwork, + ) { isEnabled, isForceHidden, wifiNetwork -> + if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) { + return@combine null + } + + val iconResId = wifiNetwork.iconResId() ?: return@combine null + val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription()) + + return@combine when { + wifiConstants.alwaysShowIconIfEnabled -> icon + !connectivityConstants.hasDataCapabilities -> icon + wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon + else -> null } } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null) - /** - * True if the activity in icon should be displayed and false otherwise. - */ - val isActivityInVisible: Flow<Boolean> - get() = - if (!constants.shouldShowActivityConfig) { - flowOf(false) - } else { - interactor.hasActivityIn + /** The wifi activity status. Null if we shouldn't display the activity status. */ + private val activity: Flow<WifiActivityModel?> = + if (!wifiConstants.shouldShowActivityConfig) { + flowOf(null) + } else { + combine(interactor.activity, interactor.ssid) { activity, ssid -> + when (ssid) { + null -> null + else -> activity + } } - .logOutputChange(logger, "activityInVisible") + } + .distinctUntilChanged() + .logOutputChange(logger, "activity") + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null) - /** The tint that should be applied to the icon. */ - val tint: Flow<Int> = if (!statusBarPipelineFlags.useNewPipelineDebugColoring()) { - emptyFlow() - } else { - flowOf(Color.CYAN) - } + private val isActivityInViewVisible: Flow<Boolean> = + activity + .map { it?.hasActivityIn == true } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) + + private val isActivityOutViewVisible: Flow<Boolean> = + activity + .map { it?.hasActivityOut == true } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) + + private val isActivityContainerVisible: Flow<Boolean> = + combine(isActivityInViewVisible, isActivityOutViewVisible) { activityIn, activityOut -> + activityIn || activityOut + } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) + + /** A view model for the status bar on the home screen. */ + val home: HomeWifiViewModel = + HomeWifiViewModel( + statusBarPipelineFlags, + wifiIcon, + isActivityInViewVisible, + isActivityOutViewVisible, + isActivityContainerVisible, + ) + + /** A view model for the status bar on keyguard. */ + val keyguard: KeyguardWifiViewModel = + KeyguardWifiViewModel( + statusBarPipelineFlags, + wifiIcon, + isActivityInViewVisible, + isActivityOutViewVisible, + isActivityContainerVisible, + ) + + /** A view model for the status bar in quick settings. */ + val qs: QsWifiViewModel = + QsWifiViewModel( + statusBarPipelineFlags, + wifiIcon, + isActivityInViewVisible, + isActivityOutViewVisible, + isActivityContainerVisible, + ) companion object { @StringRes diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 5a26d05d7b37..df10dfe9f160 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -1005,13 +1005,18 @@ public class ScreenDecorationsTest extends SysuiTestCase { assertEquals(new Size(3, 3), resDelegate.getTopRoundedSize()); assertEquals(new Size(4, 4), resDelegate.getBottomRoundedSize()); - doReturn(2f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio(); + setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + getTestsDrawable(com.android.systemui.tests.R.drawable.rounded4px) + /* roundedTopDrawable */, + getTestsDrawable(com.android.systemui.tests.R.drawable.rounded5px) + /* roundedBottomDrawable */, + 0 /* roundedPadding */, true /* privacyDot */, false /* faceScanning*/); mDisplayInfo.rotation = Surface.ROTATION_270; mScreenDecorations.onConfigurationChanged(null); - assertEquals(new Size(6, 6), resDelegate.getTopRoundedSize()); - assertEquals(new Size(8, 8), resDelegate.getBottomRoundedSize()); + assertEquals(new Size(4, 4), resDelegate.getTopRoundedSize()); + assertEquals(new Size(5, 5), resDelegate.getBottomRoundedSize()); } @Test @@ -1288,6 +1293,51 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test + public void testOnDisplayChanged_hwcLayer() { + setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + null /* roundedTopDrawable */, null /* roundedBottomDrawable */, + 0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */); + final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport(); + decorationSupport.format = PixelFormat.R_8; + doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport(); + + // top cutout + mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP)); + + mScreenDecorations.start(); + + final ScreenDecorHwcLayer hwcLayer = mScreenDecorations.mScreenDecorHwcLayer; + spyOn(hwcLayer); + doReturn(mDisplay).when(hwcLayer).getDisplay(); + + mScreenDecorations.mDisplayListener.onDisplayChanged(1); + + verify(hwcLayer, times(1)).onDisplayChanged(any()); + } + + @Test + public void testOnDisplayChanged_nonHwcLayer() { + setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, + null /* roundedTopDrawable */, null /* roundedBottomDrawable */, + 0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */); + + // top cutout + mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP)); + + mScreenDecorations.start(); + + final ScreenDecorations.DisplayCutoutView cutoutView = (ScreenDecorations.DisplayCutoutView) + mScreenDecorations.getOverlayView(R.id.display_cutout); + assertNotNull(cutoutView); + spyOn(cutoutView); + doReturn(mDisplay).when(cutoutView).getDisplay(); + + mScreenDecorations.mDisplayListener.onDisplayChanged(1); + + verify(cutoutView, times(1)).onDisplayChanged(any()); + } + + @Test public void testHasSameProvidersWithNullOverlays() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java index 08fe7c486529..2a4c0eb18d02 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java @@ -16,13 +16,14 @@ package com.android.systemui.clipboardoverlay; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import android.content.ClipData; import android.content.ComponentName; import android.content.Intent; import android.net.Uri; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import android.text.SpannableString; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -129,6 +130,18 @@ public class IntentCreatorTest extends SysuiTestCase { assertEquals("image/png", target.getType()); } + @Test + public void test_getShareIntent_spannableText() { + ClipData clipData = ClipData.newPlainText("Test", new SpannableString("Test Item")); + Intent intent = IntentCreator.getShareIntent(clipData, getContext()); + + assertEquals(Intent.ACTION_CHOOSER, intent.getAction()); + assertFlags(intent, EXTERNAL_INTENT_FLAGS); + Intent target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class); + assertEquals("Test Item", target.getStringExtra(Intent.EXTRA_TEXT)); + assertEquals("text/plain", target.getType()); + } + // Assert that the given flags are set private void assertFlags(Intent intent, int flags) { assertTrue((intent.getFlags() & flags) == flags); diff --git a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt index 93a1868b72f5..f93336134900 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt @@ -24,11 +24,12 @@ import androidx.annotation.DrawableRes import androidx.test.filters.SmallTest import com.android.internal.R as InternalR import com.android.systemui.R as SystemUIR -import com.android.systemui.SysuiTestCase import com.android.systemui.tests.R +import com.android.systemui.SysuiTestCase import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test + import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations @@ -101,11 +102,14 @@ class RoundedCornerResDelegateTest : SysuiTestCase() { assertEquals(Size(3, 3), roundedCornerResDelegate.topRoundedSize) assertEquals(Size(4, 4), roundedCornerResDelegate.bottomRoundedSize) - roundedCornerResDelegate.physicalPixelDisplaySizeRatio = 2f + setupResources(radius = 100, + roundedTopDrawable = getTestsDrawable(R.drawable.rounded4px), + roundedBottomDrawable = getTestsDrawable(R.drawable.rounded5px)) + roundedCornerResDelegate.updateDisplayUniqueId(null, 1) - assertEquals(Size(6, 6), roundedCornerResDelegate.topRoundedSize) - assertEquals(Size(8, 8), roundedCornerResDelegate.bottomRoundedSize) + assertEquals(Size(4, 4), roundedCornerResDelegate.topRoundedSize) + assertEquals(Size(5, 5), roundedCornerResDelegate.bottomRoundedSize) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index b42b7695cedd..8b1554c1f66f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -48,6 +48,7 @@ import android.view.WindowManagerPolicyConstants; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; +import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; @@ -234,6 +235,11 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback(any()); } + /** + * This specific test case appears to be flaky. + * b/249136797 tracks the task of root-causing and fixing it. + */ + @FlakyTest @Test public void testPredictiveBackInvocationDismissesDialog() { mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt index e3e3b7413157..5ad354247a04 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt @@ -30,7 +30,6 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq @@ -46,7 +45,6 @@ import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -73,10 +71,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var dumpManager: DumpManager @Mock lateinit var logger: MediaUiEventLogger @Mock lateinit var debugLogger: MediaCarouselControllerLogger - @Mock lateinit var mediaViewHolder: MediaViewHolder - @Mock lateinit var player: TransitionLayout - @Mock lateinit var recommendationViewHolder: RecommendationViewHolder - @Mock lateinit var recommendations: TransitionLayout @Mock lateinit var mediaPlayer: MediaControlPanel @Mock lateinit var mediaViewController: MediaViewController @Mock lateinit var smartspaceMediaData: SmartspaceMediaData @@ -282,46 +276,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!)) } - @Test - fun testSetSquishinessFractionForMedia_setPlayerBottom() { - whenever(panel.mediaViewHolder).thenReturn(mediaViewHolder) - whenever(mediaViewHolder.player).thenReturn(player) - whenever(player.measuredHeight).thenReturn(100) - - val playingLocal = Triple("playing local", - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false), - 4500L) - MediaPlayerData.addMediaPlayer(playingLocal.first, playingLocal.second, panel, clock, - false, debugLogger) - - mediaCarouselController.squishinessFraction = 0.0f - verify(player).bottom = 50 - verifyNoMoreInteractions(recommendationViewHolder) - - mediaCarouselController.squishinessFraction = 0.5f - verify(player).bottom = 75 - verifyNoMoreInteractions(recommendationViewHolder) - } - - @Test - fun testSetSquishinessFractionForRecommendation_setPlayerBottom() { - whenever(panel.recommendationViewHolder).thenReturn(recommendationViewHolder) - whenever(recommendationViewHolder.recommendations).thenReturn(recommendations) - whenever(recommendations.measuredHeight).thenReturn(100) - - MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel, - false, clock) - - mediaCarouselController.squishinessFraction = 0.0f - verifyNoMoreInteractions(mediaViewHolder) - verify(recommendationViewHolder.recommendations).bottom = 50 - - mediaCarouselController.squishinessFraction = 0.5f - verifyNoMoreInteractions(mediaViewHolder) - verify(recommendationViewHolder.recommendations).bottom = 75 - } - fun testMediaLoaded_ScrollToActivePlayer() { listener.value.onMediaDataLoaded("playing local", null, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java index cbe118635e95..3cad2a005882 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java @@ -44,7 +44,6 @@ import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaCarouselController; import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; @@ -87,7 +86,6 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { @Mock private QSLogger mQSLogger; private DumpManager mDumpManager = new DumpManager(); - private MediaCarouselController mMediaCarouselController; @Mock QSTileImpl mQSTile; @Mock @@ -110,9 +108,9 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { protected TestableQSPanelControllerBase(QSPanel view, QSTileHost host, QSCustomizerController qsCustomizerController, MediaHost mediaHost, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, - DumpManager dumpManager, MediaCarouselController mediaCarouselController) { + DumpManager dumpManager) { super(view, host, qsCustomizerController, true, mediaHost, metricsLogger, uiEventLogger, - qsLogger, dumpManager, mediaCarouselController); + qsLogger, dumpManager); } @Override @@ -146,7 +144,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { mController = new TestableQSPanelControllerBase(mQSPanel, mQSTileHost, mQSCustomizerController, mMediaHost, - mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager, mMediaCarouselController); + mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager); mController.init(); reset(mQSTileRevealController); @@ -158,7 +156,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { QSPanelControllerBase<QSPanel> controller = new TestableQSPanelControllerBase(mQSPanel, mQSTileHost, mQSCustomizerController, mMediaHost, - mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager, mMediaCarouselController) { + mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager) { @Override protected QSTileRevealController createTileRevealController() { return mQSTileRevealController; @@ -253,7 +251,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { when(mQSPanel.getDumpableTag()).thenReturn("QSPanelLandscape"); mController = new TestableQSPanelControllerBase(mQSPanel, mQSTileHost, mQSCustomizerController, mMediaHost, - mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager, mMediaCarouselController); + mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager); mController.init(); assertThat(mController.shouldUseHorizontalLayout()).isTrue(); @@ -262,7 +260,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { when(mQSPanel.getDumpableTag()).thenReturn("QSPanelPortrait"); mController = new TestableQSPanelControllerBase(mQSPanel, mQSTileHost, mQSCustomizerController, mMediaHost, - mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager, mMediaCarouselController); + mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager); mController.init(); assertThat(mController.shouldUseHorizontalLayout()).isFalse(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt index 98d499a70fa7..5eb9a9862340 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt @@ -6,7 +6,6 @@ import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.media.MediaCarouselController import com.android.systemui.media.MediaHost import com.android.systemui.media.MediaHostState import com.android.systemui.plugins.FalsingManager @@ -41,7 +40,6 @@ class QSPanelControllerTest : SysuiTestCase() { @Mock private lateinit var qsCustomizerController: QSCustomizerController @Mock private lateinit var qsTileRevealControllerFactory: QSTileRevealController.Factory @Mock private lateinit var dumpManager: DumpManager - @Mock private lateinit var mediaCarouselController: MediaCarouselController @Mock private lateinit var metricsLogger: MetricsLogger @Mock private lateinit var uiEventLogger: UiEventLogger @Mock private lateinit var qsLogger: QSLogger @@ -78,7 +76,6 @@ class QSPanelControllerTest : SysuiTestCase() { mediaHost, qsTileRevealControllerFactory, dumpManager, - mediaCarouselController, metricsLogger, uiEventLogger, qsLogger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java index 233c267c3be0..1c686c66e31e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java @@ -726,7 +726,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { when(mSecurityController.isParentalControlsEnabled()).thenReturn(true); when(mSecurityController.getLabel(any())).thenReturn(PARENTAL_CONTROLS_LABEL); - View view = mFooterUtils.createDialogView(); + View view = mFooterUtils.createDialogView(getContext()); TextView textView = (TextView) view.findViewById(R.id.parental_controls_title); assertEquals(PARENTAL_CONTROLS_LABEL, textView.getText()); } @@ -749,7 +749,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT)) .thenReturn(DEVICE_OWNER_TYPE_FINANCED); - View view = mFooterUtils.createDialogView(); + View view = mFooterUtils.createDialogView(getContext()); TextView managementSubtitle = view.findViewById(R.id.device_management_subtitle); assertEquals(View.VISIBLE, managementSubtitle.getVisibility()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt index 4af5b9018d5a..6af8e4904a1e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt @@ -23,7 +23,6 @@ import com.android.internal.logging.MetricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.media.MediaCarouselController import com.android.systemui.media.MediaHost import com.android.systemui.media.MediaHostState import com.android.systemui.plugins.qs.QSTile @@ -60,7 +59,6 @@ class QuickQSPanelControllerTest : SysuiTestCase() { @Mock private lateinit var tileLayout: TileLayout @Mock private lateinit var tileView: QSTileView @Captor private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener> - @Mock private lateinit var mediaCarouselController: MediaCarouselController private val uiEventLogger = UiEventLoggerFake() private val dumpManager = DumpManager() @@ -90,8 +88,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() { metricsLogger, uiEventLogger, qsLogger, - dumpManager, - mediaCarouselController) + dumpManager) controller.init() } @@ -158,8 +155,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() { metricsLogger: MetricsLogger, uiEventLogger: UiEventLoggerFake, qsLogger: QSLogger, - dumpManager: DumpManager, - mediaCarouselController: MediaCarouselController + dumpManager: DumpManager ) : QuickQSPanelController( view, @@ -171,8 +167,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() { metricsLogger, uiEventLogger, qsLogger, - dumpManager, - mediaCarouselController) { + dumpManager) { private var rotation = RotationUtils.ROTATION_NONE diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt index eb907bd92471..39d89bf99af2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt @@ -110,7 +110,7 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() { `when`(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController) `when`(variableDateViewControllerFactory.create(any())) .thenReturn(variableDateViewController) - `when`(iconManagerFactory.create(any())).thenReturn(iconManager) + `when`(iconManagerFactory.create(any(), any())).thenReturn(iconManager) `when`(view.resources).thenReturn(mContext.resources) `when`(view.isAttachedToWindow).thenReturn(true) `when`(view.context).thenReturn(context) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt index c4485389d646..c76d9e7a2b20 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt @@ -176,7 +176,7 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() { } whenever(view.visibility).thenAnswer { _ -> viewVisibility } - whenever(iconManagerFactory.create(any())).thenReturn(iconManager) + whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager) whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(true) whenever(featureFlags.isEnabled(Flags.NEW_HEADER)).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt index 5ecfc8eb3649..90ae693db955 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt @@ -97,7 +97,7 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { whenever(view.visibility).thenAnswer { _ -> viewVisibility } whenever(variableDateViewControllerFactory.create(any())) .thenReturn(variableDateViewController) - whenever(iconManagerFactory.create(any())).thenReturn(iconManager) + whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager) whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(false) mLargeScreenShadeHeaderController = LargeScreenShadeHeaderController( view, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index ba5f5038c1d9..cfaa4707ef76 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -135,7 +135,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); - when(mIconManagerFactory.create(any())).thenReturn(mIconManager); + when(mIconManagerFactory.create(any(), any())).thenReturn(mIconManager); allowTestableLooperAsMainThread(); TestableLooper.get(this).runWithLooper(() -> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java index de7db74495af..34399b80c9f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java @@ -51,8 +51,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import javax.inject.Provider; - @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest @@ -79,8 +77,9 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { LinearLayout layout = new LinearLayout(mContext); TestDarkIconManager manager = new TestDarkIconManager( layout, + StatusBarLocation.HOME, mock(StatusBarPipelineFlags.class), - () -> mock(WifiViewModel.class), + mock(WifiViewModel.class), mMobileContextProvider, mock(DarkIconDispatcher.class)); testCallOnAdd_forManager(manager); @@ -121,13 +120,15 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { TestDarkIconManager( LinearLayout group, + StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, - Provider<WifiViewModel> wifiViewModelProvider, + WifiViewModel wifiViewModel, MobileContextProvider contextProvider, DarkIconDispatcher darkIconDispatcher) { super(group, + location, statusBarPipelineFlags, - wifiViewModelProvider, + wifiViewModel, contextProvider, darkIconDispatcher); } @@ -165,8 +166,9 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { private static class TestIconManager extends IconManager implements TestableIconManager { TestIconManager(ViewGroup group, MobileContextProvider contextProvider) { super(group, + StatusBarLocation.HOME, mock(StatusBarPipelineFlags.class), - () -> mock(WifiViewModel.class), + mock(WifiViewModel.class), contextProvider); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index ee4b9d9c93f2..dcce61b86ced 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -26,7 +26,6 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -526,21 +525,4 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mBouncerExpansionCallback.onVisibilityChanged(false); verify(mCentralSurfaces).setBouncerShowingOverDream(false); } - - @Test - public void testSetDozing_Dozing() { - clearInvocations(mBouncer); - mStatusBarKeyguardViewManager.onDozingChanged(true); - // Once when shown and once with dozing changed. - verify(mBouncer, times(1)).hide(false); - } - - @Test - public void testSetDozing_notDozing() { - mStatusBarKeyguardViewManager.onDozingChanged(true); - clearInvocations(mBouncer); - mStatusBarKeyguardViewManager.onDozingChanged(false); - // Once when shown and twice with dozing changed. - verify(mBouncer, times(1)).hide(false); - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 37c8f6285970..a3c6e9514191 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -431,7 +431,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mOperatorNameViewControllerFactory = mock(OperatorNameViewController.Factory.class); when(mOperatorNameViewControllerFactory.create(any())) .thenReturn(mOperatorNameViewController); - when(mIconManagerFactory.create(any())).thenReturn(mIconManager); + when(mIconManagerFactory.create(any(), any())).thenReturn(mIconManager); mSecureSettings = mock(SecureSettings.class); setUpNotificationIconAreaController(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt index 36be1be309d6..0e75c74ef6f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt @@ -23,9 +23,16 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.runBlocking import org.junit.Test import org.mockito.Mockito import org.mockito.Mockito.mock @@ -64,12 +71,70 @@ class ConnectivityPipelineLoggerTest : SysuiTestCase() { assertThat(actualString).contains(expectedNetId) } - private val NET_1_ID = 100 - private val NET_1 = com.android.systemui.util.mockito.mock<Network>().also { - Mockito.`when`(it.getNetId()).thenReturn(NET_1_ID) + @Test + fun logOutputChange_printsValuesAndNulls() = runBlocking(IMMEDIATE) { + val flow: Flow<Int?> = flowOf(1, null, 3) + + val job = flow + .logOutputChange(logger, "testInts") + .launchIn(this) + + val stringWriter = StringWriter() + buffer.dump(PrintWriter(stringWriter), tailLength = 0) + val actualString = stringWriter.toString() + + assertThat(actualString).contains("1") + assertThat(actualString).contains("null") + assertThat(actualString).contains("3") + + job.cancel() + } + + @Test + fun logInputChange_unit_printsInputName() = runBlocking(IMMEDIATE) { + val flow: Flow<Unit> = flowOf(Unit, Unit) + + val job = flow + .logInputChange(logger, "testInputs") + .launchIn(this) + + val stringWriter = StringWriter() + buffer.dump(PrintWriter(stringWriter), tailLength = 0) + val actualString = stringWriter.toString() + + assertThat(actualString).contains("testInputs") + + job.cancel() + } + + @Test + fun logInputChange_any_printsValuesAndNulls() = runBlocking(IMMEDIATE) { + val flow: Flow<Any?> = flowOf(null, 2, "threeString") + + val job = flow + .logInputChange(logger, "testInputs") + .launchIn(this) + + val stringWriter = StringWriter() + buffer.dump(PrintWriter(stringWriter), tailLength = 0) + val actualString = stringWriter.toString() + + assertThat(actualString).contains("null") + assertThat(actualString).contains("2") + assertThat(actualString).contains("threeString") + + job.cancel() + } + + companion object { + private const val NET_1_ID = 100 + private val NET_1 = com.android.systemui.util.mockito.mock<Network>().also { + Mockito.`when`(it.getNetId()).thenReturn(NET_1_ID) + } + private val NET_1_CAPS = NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build() + private val IMMEDIATE = Dispatchers.Main.immediate } - private val NET_1_CAPS = NetworkCapabilities.Builder() - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt index 6b8d4aa7c51f..f751afc195b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt @@ -16,20 +16,27 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT -import kotlinx.coroutines.flow.Flow +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow /** Fake implementation of [WifiRepository] exposing set methods for all the flows. */ class FakeWifiRepository : WifiRepository { + private val _isWifiEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled + private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> = MutableStateFlow(WifiNetworkModel.Inactive) - override val wifiNetwork: Flow<WifiNetworkModel> = _wifiNetwork + override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork private val _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT) - override val wifiActivity: Flow<WifiActivityModel> = _wifiActivity + override val wifiActivity: StateFlow<WifiActivityModel> = _wifiActivity + + fun setIsWifiEnabled(enabled: Boolean) { + _isWifiEnabled.value = enabled + } fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) { _wifiNetwork.value = wifiNetworkModel diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt index d070ba0e47be..0ba0bd623c39 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt @@ -28,15 +28,17 @@ import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.concurrent.Executor @@ -44,23 +46,28 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Before import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @SmallTest class WifiRepositoryImplTest : SysuiTestCase() { private lateinit var underTest: WifiRepositoryImpl + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var wifiManager: WifiManager @@ -70,16 +77,17 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever( + broadcastDispatcher.broadcastFlow( + any(), + nullable(), + anyInt(), + nullable(), + ) + ).thenReturn(flowOf(Unit)) executor = FakeExecutor(FakeSystemClock()) scope = CoroutineScope(IMMEDIATE) - - underTest = WifiRepositoryImpl( - connectivityManager, - logger, - executor, - scope, - wifiManager, - ) + underTest = createRepo() } @After @@ -88,6 +96,132 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test + fun isWifiEnabled_nullWifiManager_getsFalse() = runBlocking(IMMEDIATE) { + underTest = createRepo(wifiManagerToUse = null) + + assertThat(underTest.isWifiEnabled.value).isFalse() + } + + @Test + fun isWifiEnabled_initiallyGetsWifiManagerValue() = runBlocking(IMMEDIATE) { + whenever(wifiManager.isWifiEnabled).thenReturn(true) + + underTest = createRepo() + + assertThat(underTest.isWifiEnabled.value).isTrue() + } + + @Test + fun isWifiEnabled_networkCapabilitiesChanged_valueUpdated() = runBlocking(IMMEDIATE) { + // We need to call launch on the flows so that they start updating + val networkJob = underTest.wifiNetwork.launchIn(this) + val enabledJob = underTest.isWifiEnabled.launchIn(this) + + whenever(wifiManager.isWifiEnabled).thenReturn(true) + getNetworkCallback().onCapabilitiesChanged( + NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO) + ) + + assertThat(underTest.isWifiEnabled.value).isTrue() + + whenever(wifiManager.isWifiEnabled).thenReturn(false) + getNetworkCallback().onCapabilitiesChanged( + NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO) + ) + + assertThat(underTest.isWifiEnabled.value).isFalse() + + networkJob.cancel() + enabledJob.cancel() + } + + @Test + fun isWifiEnabled_networkLost_valueUpdated() = runBlocking(IMMEDIATE) { + // We need to call launch on the flows so that they start updating + val networkJob = underTest.wifiNetwork.launchIn(this) + val enabledJob = underTest.isWifiEnabled.launchIn(this) + + whenever(wifiManager.isWifiEnabled).thenReturn(true) + getNetworkCallback().onLost(NETWORK) + + assertThat(underTest.isWifiEnabled.value).isTrue() + + whenever(wifiManager.isWifiEnabled).thenReturn(false) + getNetworkCallback().onLost(NETWORK) + + assertThat(underTest.isWifiEnabled.value).isFalse() + + networkJob.cancel() + enabledJob.cancel() + } + + @Test + fun isWifiEnabled_intentsReceived_valueUpdated() = runBlocking(IMMEDIATE) { + val intentFlow = MutableSharedFlow<Unit>() + whenever( + broadcastDispatcher.broadcastFlow( + any(), + nullable(), + anyInt(), + nullable(), + ) + ).thenReturn(intentFlow) + underTest = createRepo() + + val job = underTest.isWifiEnabled.launchIn(this) + + whenever(wifiManager.isWifiEnabled).thenReturn(true) + intentFlow.emit(Unit) + + assertThat(underTest.isWifiEnabled.value).isTrue() + + whenever(wifiManager.isWifiEnabled).thenReturn(false) + intentFlow.emit(Unit) + + assertThat(underTest.isWifiEnabled.value).isFalse() + + job.cancel() + } + + @Test + fun isWifiEnabled_bothIntentAndNetworkUpdates_valueAlwaysUpdated() = runBlocking(IMMEDIATE) { + val intentFlow = MutableSharedFlow<Unit>() + whenever( + broadcastDispatcher.broadcastFlow( + any(), + nullable(), + anyInt(), + nullable(), + ) + ).thenReturn(intentFlow) + underTest = createRepo() + + val networkJob = underTest.wifiNetwork.launchIn(this) + val enabledJob = underTest.isWifiEnabled.launchIn(this) + + whenever(wifiManager.isWifiEnabled).thenReturn(false) + intentFlow.emit(Unit) + assertThat(underTest.isWifiEnabled.value).isFalse() + + whenever(wifiManager.isWifiEnabled).thenReturn(true) + getNetworkCallback().onLost(NETWORK) + assertThat(underTest.isWifiEnabled.value).isTrue() + + whenever(wifiManager.isWifiEnabled).thenReturn(false) + getNetworkCallback().onCapabilitiesChanged( + NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO) + ) + assertThat(underTest.isWifiEnabled.value).isFalse() + + whenever(wifiManager.isWifiEnabled).thenReturn(true) + intentFlow.emit(Unit) + assertThat(underTest.isWifiEnabled.value).isTrue() + + networkJob.cancel() + enabledJob.cancel() + } + + @Test fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) { var latest: WifiNetworkModel? = null val job = underTest @@ -509,13 +643,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) { - underTest = WifiRepositoryImpl( - connectivityManager, - logger, - executor, - scope, - wifiManager = null, - ) + underTest = createRepo(wifiManagerToUse = null) var latest: WifiActivityModel? = null val job = underTest @@ -594,6 +722,17 @@ class WifiRepositoryImplTest : SysuiTestCase() { job.cancel() } + private fun createRepo(wifiManagerToUse: WifiManager? = wifiManager): WifiRepositoryImpl { + return WifiRepositoryImpl( + broadcastDispatcher, + connectivityManager, + logger, + executor, + scope, + wifiManagerToUse, + ) + } + private fun getTrafficStateCallback(): TrafficStateCallback { val callbackCaptor = argumentCaptor<TrafficStateCallback>() verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt index e896749d9a94..39b886af1cb8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt @@ -16,13 +16,14 @@ package com.android.systemui.statusbar.pipeline.wifi.domain.interactor +import android.net.wifi.WifiManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -50,172 +51,129 @@ class WifiInteractorTest : SysuiTestCase() { } @Test - fun hasActivityIn_noInOrOut_outputsFalse() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = false, hasActivityOut = false) - ) + fun ssid_inactiveNetwork_outputsNull() = runBlocking(IMMEDIATE) { + wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) - var latest: Boolean? = null + var latest: String? = "default" val job = underTest - .hasActivityIn - .onEach { latest = it } - .launchIn(this) + .ssid + .onEach { latest = it } + .launchIn(this) - assertThat(latest).isFalse() + assertThat(latest).isNull() job.cancel() } @Test - fun hasActivityIn_onlyOut_outputsFalse() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = false, hasActivityOut = true) - ) + fun ssid_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) { + wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged) - var latest: Boolean? = null + var latest: String? = "default" val job = underTest - .hasActivityIn - .onEach { latest = it } - .launchIn(this) + .ssid + .onEach { latest = it } + .launchIn(this) - assertThat(latest).isFalse() + assertThat(latest).isNull() job.cancel() } @Test - fun hasActivityIn_onlyIn_outputsTrue() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = true, hasActivityOut = false) - ) - - var latest: Boolean? = null + fun ssid_isPasspointAccessPoint_outputsPasspointName() = runBlocking(IMMEDIATE) { + wifiRepository.setWifiNetwork(WifiNetworkModel.Active( + networkId = 1, + isPasspointAccessPoint = true, + passpointProviderFriendlyName = "friendly", + )) + + var latest: String? = null val job = underTest - .hasActivityIn - .onEach { latest = it } - .launchIn(this) + .ssid + .onEach { latest = it } + .launchIn(this) - assertThat(latest).isTrue() + assertThat(latest).isEqualTo("friendly") job.cancel() } @Test - fun hasActivityIn_inAndOut_outputsTrue() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = true, hasActivityOut = true) - ) - - var latest: Boolean? = null + fun ssid_isOnlineSignUpForPasspoint_outputsPasspointName() = runBlocking(IMMEDIATE) { + wifiRepository.setWifiNetwork(WifiNetworkModel.Active( + networkId = 1, + isOnlineSignUpForPasspointAccessPoint = true, + passpointProviderFriendlyName = "friendly", + )) + + var latest: String? = null val job = underTest - .hasActivityIn - .onEach { latest = it } - .launchIn(this) + .ssid + .onEach { latest = it } + .launchIn(this) - assertThat(latest).isTrue() + assertThat(latest).isEqualTo("friendly") job.cancel() } @Test - fun hasActivityIn_ssidNull_outputsFalse() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = null)) - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = true, hasActivityOut = true) - ) + fun ssid_unknownSsid_outputsNull() = runBlocking(IMMEDIATE) { + wifiRepository.setWifiNetwork(WifiNetworkModel.Active( + networkId = 1, + ssid = WifiManager.UNKNOWN_SSID, + )) - var latest: Boolean? = null + var latest: String? = "default" val job = underTest - .hasActivityIn - .onEach { latest = it } - .launchIn(this) - - assertThat(latest).isFalse() - - job.cancel() - } - - @Test - fun hasActivityIn_inactiveNetwork_outputsFalse() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = true, hasActivityOut = true) - ) - - var latest: Boolean? = null - val job = underTest - .hasActivityIn + .ssid .onEach { latest = it } .launchIn(this) - assertThat(latest).isFalse() + assertThat(latest).isNull() job.cancel() } @Test - fun hasActivityIn_carrierMergedNetwork_outputsFalse() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged) - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = true, hasActivityOut = true) - ) + fun ssid_validSsid_outputsSsid() = runBlocking(IMMEDIATE) { + wifiRepository.setWifiNetwork(WifiNetworkModel.Active( + networkId = 1, + ssid = "MyAwesomeWifiNetwork", + )) - var latest: Boolean? = null + var latest: String? = null val job = underTest - .hasActivityIn + .ssid .onEach { latest = it } .launchIn(this) - assertThat(latest).isFalse() + assertThat(latest).isEqualTo("MyAwesomeWifiNetwork") job.cancel() } @Test - fun hasActivityIn_multipleChanges_multipleOutputChanges() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) - + fun isEnabled_matchesRepoIsEnabled() = runBlocking(IMMEDIATE) { var latest: Boolean? = null val job = underTest - .hasActivityIn - .onEach { latest = it } - .launchIn(this) + .isEnabled + .onEach { latest = it } + .launchIn(this) - // Conduct a series of changes and verify we catch each of them in succession - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = true, hasActivityOut = false) - ) + wifiRepository.setIsWifiEnabled(true) yield() assertThat(latest).isTrue() - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = false, hasActivityOut = true) - ) + wifiRepository.setIsWifiEnabled(false) yield() assertThat(latest).isFalse() - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = true, hasActivityOut = true) - ) + wifiRepository.setIsWifiEnabled(true) yield() assertThat(latest).isTrue() - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = true, hasActivityOut = false) - ) - yield() - assertThat(latest).isTrue() - - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = false, hasActivityOut = false) - ) - yield() - assertThat(latest).isFalse() - job.cancel() } @@ -242,6 +200,32 @@ class WifiInteractorTest : SysuiTestCase() { } @Test + fun activity_matchesRepoWifiActivity() = runBlocking(IMMEDIATE) { + var latest: WifiActivityModel? = null + val job = underTest + .activity + .onEach { latest = it } + .launchIn(this) + + val activity1 = WifiActivityModel(hasActivityIn = true, hasActivityOut = true) + wifiRepository.setWifiActivity(activity1) + yield() + assertThat(latest).isEqualTo(activity1) + + val activity2 = WifiActivityModel(hasActivityIn = false, hasActivityOut = false) + wifiRepository.setWifiActivity(activity2) + yield() + assertThat(latest).isEqualTo(activity2) + + val activity3 = WifiActivityModel(hasActivityIn = true, hasActivityOut = false) + wifiRepository.setWifiActivity(activity3) + yield() + assertThat(latest).isEqualTo(activity3) + + job.cancel() + } + + @Test fun isForceHidden_repoHasWifiHidden_outputsTrue() = runBlocking(IMMEDIATE) { connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI)) @@ -270,10 +254,6 @@ class WifiInteractorTest : SysuiTestCase() { job.cancel() } - - companion object { - val VALID_WIFI_NETWORK_MODEL = WifiNetworkModel.Active(networkId = 1, ssid = "AB") - } } private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt index 3c200a5da4fa..4efb13520ebf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt @@ -16,38 +16,225 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.view +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper +import android.testing.ViewUtils +import android.view.View import androidx.test.filters.SmallTest +import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.lifecycle.InstantTaskExecutorRule -import com.android.systemui.util.Assert -import com.android.systemui.util.mockito.mock +import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT +import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor +import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants +import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.MockitoAnnotations @SmallTest -@RunWith(JUnit4::class) -@RunWithLooper +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) class ModernStatusBarWifiViewTest : SysuiTestCase() { + private lateinit var testableLooper: TestableLooper + + @Mock + private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags + @Mock + private lateinit var logger: ConnectivityPipelineLogger + @Mock + private lateinit var connectivityConstants: ConnectivityConstants + @Mock + private lateinit var wifiConstants: WifiConstants + private lateinit var connectivityRepository: FakeConnectivityRepository + private lateinit var wifiRepository: FakeWifiRepository + private lateinit var interactor: WifiInteractor + private lateinit var viewModel: WifiViewModel + private lateinit var scope: CoroutineScope + @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() @Before fun setUp() { - Assert.setTestThread(Thread.currentThread()) + MockitoAnnotations.initMocks(this) + testableLooper = TestableLooper.get(this) + + connectivityRepository = FakeConnectivityRepository() + wifiRepository = FakeWifiRepository() + wifiRepository.setIsWifiEnabled(true) + interactor = WifiInteractor(connectivityRepository, wifiRepository) + scope = CoroutineScope(Dispatchers.Unconfined) + viewModel = WifiViewModel( + connectivityConstants, + context, + logger, + interactor, + scope, + statusBarPipelineFlags, + wifiConstants, + ) } @Test fun constructAndBind_hasCorrectSlot() { val view = ModernStatusBarWifiView.constructAndBind( - context, "slotName", mock() + context, "slotName", viewModel, StatusBarLocation.HOME ) assertThat(view.slot).isEqualTo("slotName") } + + @Test + fun getVisibleState_icon_returnsIcon() { + val view = ModernStatusBarWifiView.constructAndBind( + context, SLOT_NAME, viewModel, StatusBarLocation.HOME + ) + + view.setVisibleState(STATE_ICON, /* animate= */ false) + + assertThat(view.visibleState).isEqualTo(STATE_ICON) + } + + @Test + fun getVisibleState_dot_returnsDot() { + val view = ModernStatusBarWifiView.constructAndBind( + context, SLOT_NAME, viewModel, StatusBarLocation.HOME + ) + + view.setVisibleState(STATE_DOT, /* animate= */ false) + + assertThat(view.visibleState).isEqualTo(STATE_DOT) + } + + @Test + fun getVisibleState_hidden_returnsHidden() { + val view = ModernStatusBarWifiView.constructAndBind( + context, SLOT_NAME, viewModel, StatusBarLocation.HOME + ) + + view.setVisibleState(STATE_HIDDEN, /* animate= */ false) + + assertThat(view.visibleState).isEqualTo(STATE_HIDDEN) + } + + // Note: The following tests are more like integration tests, since they stand up a full + // [WifiViewModel] and test the interactions between the view, view-binder, and view-model. + + @Test + fun setVisibleState_icon_iconShownDotHidden() { + val view = ModernStatusBarWifiView.constructAndBind( + context, SLOT_NAME, viewModel, StatusBarLocation.HOME + ) + + view.setVisibleState(STATE_ICON, /* animate= */ false) + + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + assertThat(view.getIconGroupView().visibility).isEqualTo(View.VISIBLE) + assertThat(view.getDotView().visibility).isEqualTo(View.GONE) + + ViewUtils.detachView(view) + } + + @Test + fun setVisibleState_dot_iconHiddenDotShown() { + val view = ModernStatusBarWifiView.constructAndBind( + context, SLOT_NAME, viewModel, StatusBarLocation.HOME + ) + + view.setVisibleState(STATE_DOT, /* animate= */ false) + + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + assertThat(view.getIconGroupView().visibility).isEqualTo(View.GONE) + assertThat(view.getDotView().visibility).isEqualTo(View.VISIBLE) + + ViewUtils.detachView(view) + } + + @Test + fun setVisibleState_hidden_iconAndDotHidden() { + val view = ModernStatusBarWifiView.constructAndBind( + context, SLOT_NAME, viewModel, StatusBarLocation.HOME + ) + + view.setVisibleState(STATE_HIDDEN, /* animate= */ false) + + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + assertThat(view.getIconGroupView().visibility).isEqualTo(View.GONE) + assertThat(view.getDotView().visibility).isEqualTo(View.GONE) + + ViewUtils.detachView(view) + } + + @Test + fun isIconVisible_notEnabled_outputsFalse() { + wifiRepository.setIsWifiEnabled(false) + wifiRepository.setWifiNetwork( + WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2) + ) + + val view = ModernStatusBarWifiView.constructAndBind( + context, SLOT_NAME, viewModel, StatusBarLocation.HOME + ) + + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + assertThat(view.isIconVisible).isFalse() + + ViewUtils.detachView(view) + } + + @Test + fun isIconVisible_enabled_outputsTrue() { + wifiRepository.setIsWifiEnabled(true) + wifiRepository.setWifiNetwork( + WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2) + ) + + val view = ModernStatusBarWifiView.constructAndBind( + context, SLOT_NAME, viewModel, StatusBarLocation.HOME + ) + + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + assertThat(view.isIconVisible).isTrue() + + ViewUtils.detachView(view) + } + + private fun View.getIconGroupView(): View { + return this.requireViewById(R.id.wifi_group) + } + + private fun View.getDotView(): View { + return this.requireViewById(R.id.status_bar_dot) + } } + +private const val SLOT_NAME = "TestSlotName" +private const val NETWORK_ID = 200 diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt new file mode 100644 index 000000000000..929e5294de3d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel + +import android.content.Context +import androidx.annotation.DrawableRes +import androidx.test.filters.SmallTest +import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH +import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.statusbar.connectivity.WifiIcons +import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS +import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor +import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants +import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel.Companion.NO_INTERNET +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters +import org.mockito.Mock +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(Parameterized::class) +internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase) : + SysuiTestCase() { + + private lateinit var underTest: WifiViewModel + + @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var connectivityConstants: ConnectivityConstants + @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var connectivityRepository: FakeConnectivityRepository + private lateinit var wifiRepository: FakeWifiRepository + private lateinit var interactor: WifiInteractor + private lateinit var scope: CoroutineScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + connectivityRepository = FakeConnectivityRepository() + wifiRepository = FakeWifiRepository() + wifiRepository.setIsWifiEnabled(true) + interactor = WifiInteractor(connectivityRepository, wifiRepository) + scope = CoroutineScope(IMMEDIATE) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun wifiIcon() = + runBlocking(IMMEDIATE) { + wifiRepository.setIsWifiEnabled(testCase.enabled) + connectivityRepository.setForceHiddenIcons( + if (testCase.forceHidden) { + setOf(ConnectivitySlot.WIFI) + } else { + setOf() + } + ) + whenever(wifiConstants.alwaysShowIconIfEnabled) + .thenReturn(testCase.alwaysShowIconWhenEnabled) + whenever(connectivityConstants.hasDataCapabilities) + .thenReturn(testCase.hasDataCapabilities) + underTest = + WifiViewModel( + connectivityConstants, + context, + logger, + interactor, + scope, + statusBarPipelineFlags, + wifiConstants, + ) + + val iconFlow = underTest.home.wifiIcon + val job = iconFlow.launchIn(this) + + // WHEN we set a certain network + wifiRepository.setWifiNetwork(testCase.network) + yield() + + // THEN we get the expected icon + assertThat(iconFlow.value?.res).isEqualTo(testCase.expected?.iconResource) + val expectedContentDescription = + if (testCase.expected == null) { + null + } else { + testCase.expected.contentDescription.invoke(context) + } + assertThat(iconFlow.value?.contentDescription?.getAsString()) + .isEqualTo(expectedContentDescription) + + job.cancel() + } + + private fun ContentDescription.getAsString(): String? { + return when (this) { + is ContentDescription.Loaded -> this.description + is ContentDescription.Resource -> context.getString(this.res) + } + } + + internal data class Expected( + /** The resource that should be used for the icon. */ + @DrawableRes val iconResource: Int, + + /** A function that, given a context, calculates the correct content description string. */ + val contentDescription: (Context) -> String, + ) + + // Note: We use default values for the boolean parameters to reflect a "typical configuration" + // for wifi. This allows each TestCase to only define the parameter values that are critical + // for the test function. + internal data class TestCase( + val enabled: Boolean = true, + val forceHidden: Boolean = false, + val alwaysShowIconWhenEnabled: Boolean = false, + val hasDataCapabilities: Boolean = true, + val network: WifiNetworkModel, + + /** The expected output. Null if we expect the output to be null. */ + val expected: Expected? + ) + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun data(): Collection<TestCase> = + listOf( + // Enabled = false => no networks shown + TestCase( + enabled = false, + network = WifiNetworkModel.CarrierMerged, + expected = null, + ), + TestCase( + enabled = false, + network = WifiNetworkModel.Inactive, + expected = null, + ), + TestCase( + enabled = false, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 1), + expected = null, + ), + TestCase( + enabled = false, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 3), + expected = null, + ), + + // forceHidden = true => no networks shown + TestCase( + forceHidden = true, + network = WifiNetworkModel.CarrierMerged, + expected = null, + ), + TestCase( + forceHidden = true, + network = WifiNetworkModel.Inactive, + expected = null, + ), + TestCase( + enabled = false, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 2), + expected = null, + ), + TestCase( + forceHidden = true, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 1), + expected = null, + ), + + // alwaysShowIconWhenEnabled = true => all Inactive and Active networks shown + TestCase( + alwaysShowIconWhenEnabled = true, + network = WifiNetworkModel.Inactive, + expected = + Expected( + iconResource = WifiIcons.WIFI_NO_NETWORK, + contentDescription = { context -> + "${context.getString(WIFI_NO_CONNECTION)}," + + context.getString(NO_INTERNET) + } + ), + ), + TestCase( + alwaysShowIconWhenEnabled = true, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 4), + expected = + Expected( + iconResource = WIFI_NO_INTERNET_ICONS[4], + contentDescription = { context -> + "${context.getString(WIFI_CONNECTION_STRENGTH[4])}," + + context.getString(NO_INTERNET) + } + ), + ), + TestCase( + alwaysShowIconWhenEnabled = true, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2), + expected = + Expected( + iconResource = WIFI_FULL_ICONS[2], + contentDescription = { context -> + context.getString(WIFI_CONNECTION_STRENGTH[2]) + } + ), + ), + + // hasDataCapabilities = false => all Inactive and Active networks shown + TestCase( + hasDataCapabilities = false, + network = WifiNetworkModel.Inactive, + expected = + Expected( + iconResource = WifiIcons.WIFI_NO_NETWORK, + contentDescription = { context -> + "${context.getString(WIFI_NO_CONNECTION)}," + + context.getString(NO_INTERNET) + } + ), + ), + TestCase( + hasDataCapabilities = false, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 2), + expected = + Expected( + iconResource = WIFI_NO_INTERNET_ICONS[2], + contentDescription = { context -> + "${context.getString(WIFI_CONNECTION_STRENGTH[2])}," + + context.getString(NO_INTERNET) + } + ), + ), + TestCase( + hasDataCapabilities = false, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 0), + expected = + Expected( + iconResource = WIFI_FULL_ICONS[0], + contentDescription = { context -> + context.getString(WIFI_CONNECTION_STRENGTH[0]) + } + ), + ), + + // network = CarrierMerged => not shown + TestCase( + network = WifiNetworkModel.CarrierMerged, + expected = null, + ), + + // network = Inactive => not shown + TestCase( + network = WifiNetworkModel.Inactive, + expected = null, + ), + + // network = Active & validated = false => not shown + TestCase( + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 3), + expected = null, + ), + + // network = Active & validated = true => shown + TestCase( + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 4), + expected = + Expected( + iconResource = WIFI_FULL_ICONS[4], + contentDescription = { context -> + context.getString(WIFI_CONNECTION_STRENGTH[4]) + } + ), + ), + + // network has null level => not shown + TestCase( + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = null), + expected = null, + ), + ) + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate +private const val NETWORK_ID = 789 diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index 43103a065e68..3169eef83f07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -17,37 +17,34 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import androidx.test.filters.SmallTest -import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH -import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon -import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS -import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS -import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants -import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel.Companion.NO_INTERNET +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking import kotlinx.coroutines.yield +import org.junit.After import org.junit.Before import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @SmallTest class WifiViewModelTest : SysuiTestCase() { @@ -56,236 +53,426 @@ class WifiViewModelTest : SysuiTestCase() { @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var logger: ConnectivityPipelineLogger - @Mock private lateinit var constants: WifiConstants + @Mock private lateinit var connectivityConstants: ConnectivityConstants + @Mock private lateinit var wifiConstants: WifiConstants private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor + private lateinit var scope: CoroutineScope @Before fun setUp() { MockitoAnnotations.initMocks(this) connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() + wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) + scope = CoroutineScope(IMMEDIATE) + createAndSetViewModel() + } - underTest = WifiViewModel( - statusBarPipelineFlags, - constants, - context, - logger, - interactor - ) + @After + fun tearDown() { + scope.cancel() } + // Note on testing: [WifiViewModel] exposes 3 different instances of + // [LocationBasedWifiViewModel]. In practice, these 3 different instances will get the exact + // same data for icon, activity, etc. flows. So, most of these tests will test just one of the + // instances. There are also some tests that verify all 3 instances received the same data. + @Test - fun wifiIcon_forceHidden_outputsNull() = runBlocking(IMMEDIATE) { - connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI)) - wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2)) + fun wifiIcon_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) { + var latestHome: Icon? = null + val jobHome = underTest + .home + .wifiIcon + .onEach { latestHome = it } + .launchIn(this) - var latest: Icon? = null - val job = underTest + var latestKeyguard: Icon? = null + val jobKeyguard = underTest + .keyguard .wifiIcon - .onEach { latest = it } + .onEach { latestKeyguard = it } .launchIn(this) - assertThat(latest).isNull() + var latestQs: Icon? = null + val jobQs = underTest + .qs + .wifiIcon + .onEach { latestQs = it } + .launchIn(this) - job.cancel() + wifiRepository.setWifiNetwork( + WifiNetworkModel.Active( + NETWORK_ID, + isValidated = true, + level = 1 + ) + ) + yield() + + assertThat(latestHome).isInstanceOf(Icon.Resource::class.java) + assertThat(latestHome).isEqualTo(latestKeyguard) + assertThat(latestKeyguard).isEqualTo(latestQs) + + jobHome.cancel() + jobKeyguard.cancel() + jobQs.cancel() } @Test - fun wifiIcon_notForceHidden_outputsVisible() = runBlocking(IMMEDIATE) { - connectivityRepository.setForceHiddenIcons(setOf()) - wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2)) + fun activity_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(false) + createAndSetViewModel() + wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) - var latest: Icon? = null - val job = underTest - .wifiIcon - .onEach { latest = it } + var activityIn: Boolean? = null + val activityInJob = underTest + .home + .isActivityInViewVisible + .onEach { activityIn = it } .launchIn(this) - assertThat(latest).isInstanceOf(Icon.Resource::class.java) + var activityOut: Boolean? = null + val activityOutJob = underTest + .home + .isActivityOutViewVisible + .onEach { activityOut = it } + .launchIn(this) - job.cancel() + var activityContainer: Boolean? = null + val activityContainerJob = underTest + .home + .isActivityContainerVisible + .onEach { activityContainer = it } + .launchIn(this) + + // Verify that on launch, we receive false. + assertThat(activityIn).isFalse() + assertThat(activityOut).isFalse() + assertThat(activityContainer).isFalse() + + activityInJob.cancel() + activityOutJob.cancel() + activityContainerJob.cancel() } @Test - fun wifiIcon_inactiveNetwork_outputsNoNetworkIcon() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) + fun activity_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(false) + createAndSetViewModel() + wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) + + var activityIn: Boolean? = null + val activityInJob = underTest + .home + .isActivityInViewVisible + .onEach { activityIn = it } + .launchIn(this) + + var activityOut: Boolean? = null + val activityOutJob = underTest + .home + .isActivityOutViewVisible + .onEach { activityOut = it } + .launchIn(this) + + var activityContainer: Boolean? = null + val activityContainerJob = underTest + .home + .isActivityContainerVisible + .onEach { activityContainer = it } + .launchIn(this) + + // WHEN we update the repo to have activity + val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true) + wifiRepository.setWifiActivity(activity) + yield() + + // THEN we didn't update to the new activity (because our config is false) + assertThat(activityIn).isFalse() + assertThat(activityOut).isFalse() + assertThat(activityContainer).isFalse() + + activityInJob.cancel() + activityOutJob.cancel() + activityContainerJob.cancel() + } + + @Test + fun activity_nullSsid_outputsFalse() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() + + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null)) + + var activityIn: Boolean? = null + val activityInJob = underTest + .home + .isActivityInViewVisible + .onEach { activityIn = it } + .launchIn(this) + + var activityOut: Boolean? = null + val activityOutJob = underTest + .home + .isActivityOutViewVisible + .onEach { activityOut = it } + .launchIn(this) + + var activityContainer: Boolean? = null + val activityContainerJob = underTest + .home + .isActivityContainerVisible + .onEach { activityContainer = it } + .launchIn(this) + + // WHEN we update the repo to have activity + val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true) + wifiRepository.setWifiActivity(activity) + yield() + + // THEN we still output false because our network's SSID is null + assertThat(activityIn).isFalse() + assertThat(activityOut).isFalse() + assertThat(activityContainer).isFalse() + + activityInJob.cancel() + activityOutJob.cancel() + activityContainerJob.cancel() + } + + @Test + fun activity_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() + wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) + + var latestHome: Boolean? = null + val jobHome = underTest + .home + .isActivityInViewVisible + .onEach { latestHome = it } + .launchIn(this) + + var latestKeyguard: Boolean? = null + val jobKeyguard = underTest + .keyguard + .isActivityInViewVisible + .onEach { latestKeyguard = it } + .launchIn(this) + + var latestQs: Boolean? = null + val jobQs = underTest + .qs + .isActivityInViewVisible + .onEach { latestQs = it } + .launchIn(this) + + val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true) + wifiRepository.setWifiActivity(activity) + yield() - var latest: Icon? = null + assertThat(latestHome).isTrue() + assertThat(latestKeyguard).isTrue() + assertThat(latestQs).isTrue() + + jobHome.cancel() + jobKeyguard.cancel() + jobQs.cancel() + } + + @Test + fun activityIn_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() + wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) + + var latest: Boolean? = null val job = underTest - .wifiIcon - .onEach { latest = it } - .launchIn(this) - - assertThat(latest).isInstanceOf(Icon.Resource::class.java) - val icon = latest as Icon.Resource - assertThat(icon.res).isEqualTo(WIFI_NO_NETWORK) - assertThat(icon.contentDescription?.getAsString()) - .contains(context.getString(WIFI_NO_CONNECTION)) - assertThat(icon.contentDescription?.getAsString()) - .contains(context.getString(NO_INTERNET)) + .home + .isActivityInViewVisible + .onEach { latest = it } + .launchIn(this) + + val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false) + wifiRepository.setWifiActivity(activity) + yield() + + assertThat(latest).isTrue() job.cancel() } @Test - fun wifiIcon_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged) + fun activityIn_hasActivityInFalse_outputsFalse() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() + wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) - var latest: Icon? = null + var latest: Boolean? = null val job = underTest - .wifiIcon + .home + .isActivityInViewVisible .onEach { latest = it } .launchIn(this) - assertThat(latest).isNull() + val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true) + wifiRepository.setWifiActivity(activity) + yield() + + assertThat(latest).isFalse() job.cancel() } @Test - fun wifiIcon_isActiveNullLevel_outputsNull() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = null)) + fun activityOut_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() + wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) - var latest: Icon? = null + var latest: Boolean? = null val job = underTest - .wifiIcon + .home + .isActivityOutViewVisible .onEach { latest = it } .launchIn(this) - assertThat(latest).isNull() + val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true) + wifiRepository.setWifiActivity(activity) + yield() + + assertThat(latest).isTrue() job.cancel() } @Test - fun wifiIcon_isActiveAndValidated_level1_outputsFull1Icon() = runBlocking(IMMEDIATE) { - val level = 1 - - wifiRepository.setWifiNetwork( - WifiNetworkModel.Active( - NETWORK_ID, - isValidated = true, - level = level - ) - ) + fun activityOut_hasActivityOutFalse_outputsFalse() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() + wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) - var latest: Icon? = null + var latest: Boolean? = null val job = underTest - .wifiIcon + .home + .isActivityOutViewVisible .onEach { latest = it } .launchIn(this) - assertThat(latest).isInstanceOf(Icon.Resource::class.java) - val icon = latest as Icon.Resource - assertThat(icon.res).isEqualTo(WIFI_FULL_ICONS[level]) - assertThat(icon.contentDescription?.getAsString()) - .contains(context.getString(WIFI_CONNECTION_STRENGTH[level])) - assertThat(icon.contentDescription?.getAsString()) - .doesNotContain(context.getString(NO_INTERNET)) + val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false) + wifiRepository.setWifiActivity(activity) + yield() + + assertThat(latest).isFalse() job.cancel() } @Test - fun wifiIcon_isActiveAndNotValidated_level4_outputsEmpty4Icon() = runBlocking(IMMEDIATE) { - val level = 4 - - wifiRepository.setWifiNetwork( - WifiNetworkModel.Active( - NETWORK_ID, - isValidated = false, - level = level - ) - ) + fun activityContainer_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() + wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) - var latest: Icon? = null + var latest: Boolean? = null val job = underTest - .wifiIcon + .home + .isActivityContainerVisible .onEach { latest = it } .launchIn(this) - assertThat(latest).isInstanceOf(Icon.Resource::class.java) - val icon = latest as Icon.Resource - assertThat(icon.res).isEqualTo(WIFI_NO_INTERNET_ICONS[level]) - assertThat(icon.contentDescription?.getAsString()) - .contains(context.getString(WIFI_CONNECTION_STRENGTH[level])) - assertThat(icon.contentDescription?.getAsString()) - .contains(context.getString(NO_INTERNET)) + val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false) + wifiRepository.setWifiActivity(activity) + yield() + + assertThat(latest).isTrue() job.cancel() } @Test - fun activityInVisible_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(constants.shouldShowActivityConfig).thenReturn(false) + fun activityContainer_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) var latest: Boolean? = null val job = underTest - .isActivityInVisible - .onEach { latest = it } - .launchIn(this) + .home + .isActivityContainerVisible + .onEach { latest = it } + .launchIn(this) - // Verify that on launch, we receive a false. - assertThat(latest).isFalse() + val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true) + wifiRepository.setWifiActivity(activity) + yield() + + assertThat(latest).isTrue() job.cancel() } @Test - fun activityInVisible_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) { - whenever(constants.shouldShowActivityConfig).thenReturn(false) + fun activityContainer_inAndOutTrue_outputsTrue() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) var latest: Boolean? = null val job = underTest - .isActivityInVisible - .onEach { latest = it } - .launchIn(this) + .home + .isActivityContainerVisible + .onEach { latest = it } + .launchIn(this) - // Update the repo to have activityIn - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = true, hasActivityOut = false) - ) + val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true) + wifiRepository.setWifiActivity(activity) yield() - // Verify that we didn't update to activityIn=true (because our config is false) - assertThat(latest).isFalse() + assertThat(latest).isTrue() job.cancel() } @Test - fun activityInVisible_showActivityConfigTrue_outputsUpdate() = runBlocking(IMMEDIATE) { - whenever(constants.shouldShowActivityConfig).thenReturn(true) + fun activityContainer_inAndOutFalse_outputsFalse() = runBlocking(IMMEDIATE) { + whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) var latest: Boolean? = null val job = underTest - .isActivityInVisible - .onEach { latest = it } - .launchIn(this) + .home + .isActivityContainerVisible + .onEach { latest = it } + .launchIn(this) - // Update the repo to have activityIn - wifiRepository.setWifiActivity( - WifiActivityModel(hasActivityIn = true, hasActivityOut = false) - ) + val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = false) + wifiRepository.setWifiActivity(activity) yield() - // Verify that we updated to activityIn=true - assertThat(latest).isTrue() + assertThat(latest).isFalse() job.cancel() } - private fun ContentDescription.getAsString(): String? { - return when (this) { - is ContentDescription.Loaded -> this.description - is ContentDescription.Resource -> context.getString(this.res) - } + private fun createAndSetViewModel() { + // [WifiViewModel] creates its flows as soon as it's instantiated, and some of those flow + // creations rely on certain config values that we mock out in individual tests. This method + // allows tests to create the view model only after those configs are correctly set up. + underTest = WifiViewModel( + connectivityConstants, + context, + logger, + interactor, + scope, + statusBarPipelineFlags, + wifiConstants, + ) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index c47ea9cea75e..6ace4044b3f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -275,10 +275,9 @@ public class RemoteInputViewTest extends SysuiTestCase { EditText editText = view.findViewById(R.id.remote_input_text); editText.setText(TEST_REPLY); ClipDescription description = new ClipDescription("", new String[] {"image/png"}); - // We need to use an (arbitrary) real resource here so that an actual image gets attached. + // We need to use an (arbitrary) real resource here so that an actual image gets attached ClipData clip = new ClipData(description, new ClipData.Item( - Uri.parse("android.resource://com.android.systemui/" - + R.drawable.default_thumbnail))); + Uri.parse("android.resource://android/" + android.R.drawable.btn_default))); ContentInfo payload = new ContentInfo.Builder(clip, SOURCE_CLIPBOARD).build(); view.setAttachment(payload); diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java index ad09f7cd3dde..33f1b4282e97 100644 --- a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java +++ b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java @@ -184,13 +184,21 @@ class BleCompanionDeviceScanner implements AssociationStore.OnChangeListener { @MainThread private void startScan() { enforceInitialized(); - // This method should not be called if scan is already in progress. - if (mScanning) throw new IllegalStateException("Scan is already in progress."); - // Neither should this method be called if the adapter is not available. - if (mBleScanner == null) throw new IllegalStateException("BLE is not available."); if (DEBUG) Log.i(TAG, "startScan()"); + // This method should not be called if scan is already in progress. + if (mScanning) { + Slog.w(TAG, "Scan is already in progress."); + return; + } + + // Neither should this method be called if the adapter is not available. + if (mBleScanner == null) { + Slog.w(TAG, "BLE is not available."); + return; + } + // Collect MAC addresses from all associations. final Set<String> macAddresses = new HashSet<>(); for (AssociationInfo association : mAssociationStore.getAssociations()) { @@ -221,8 +229,18 @@ class BleCompanionDeviceScanner implements AssociationStore.OnChangeListener { filters.add(filter); } - mBleScanner.startScan(filters, SCAN_SETTINGS, mScanCallback); - mScanning = true; + // BluetoothLeScanner will throw an IllegalStateException if startScan() is called while LE + // is not enabled. + if (mBtAdapter.isLeEnabled()) { + try { + mBleScanner.startScan(filters, SCAN_SETTINGS, mScanCallback); + mScanning = true; + } catch (IllegalStateException e) { + Slog.w(TAG, "Exception while starting BLE scanning", e); + } + } else { + Slog.w(TAG, "BLE scanning is not turned on"); + } } private void stopScanIfNeeded() { @@ -240,11 +258,11 @@ class BleCompanionDeviceScanner implements AssociationStore.OnChangeListener { if (mBtAdapter.isLeEnabled()) { try { mBleScanner.stopScan(mScanCallback); - } catch (RuntimeException e) { - // Just to be sure not to crash system server here if BluetoothLeScanner throws - // another RuntimeException. + } catch (IllegalStateException e) { Slog.w(TAG, "Exception while stopping BLE scanning", e); } + } else { + Slog.w(TAG, "BLE scanning is not turned on"); } mScanning = false; diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index efb2cb7a3283..2a219289cf10 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -411,8 +411,11 @@ final class LocalDisplayAdapter extends DisplayAdapter { // For a new display, we need to initialize the default mode ID. if (mDefaultModeId == INVALID_MODE_ID) { - mDefaultModeId = activeRecord.mMode.getModeId(); - mDefaultModeGroup = mActiveSfDisplayMode.group; + mDefaultModeId = mSystemPreferredModeId != INVALID_MODE_ID + ? mSystemPreferredModeId : activeRecord.mMode.getModeId(); + mDefaultModeGroup = mSystemPreferredModeId != INVALID_MODE_ID + ? preferredSfDisplayMode.group + : mActiveSfDisplayMode.group; } else if (modesAdded && activeModeChanged) { Slog.d(TAG, "New display modes are added and the active mode has changed, " + "use active mode as default mode."); @@ -894,13 +897,6 @@ final class LocalDisplayAdapter extends DisplayAdapter { public void setUserPreferredDisplayModeLocked(Display.Mode mode) { final int oldModeId = getPreferredModeId(); mUserPreferredMode = mode; - // When clearing the user preferred mode we need to also reset the default mode. This is - // used by DisplayModeDirector to determine the default resolution, so if we don't clear - // it then the resolution won't reset to what it would've been prior to setting a user - // preferred display mode. - if (mode == null && mSystemPreferredModeId != INVALID_MODE_ID) { - mDefaultModeId = mSystemPreferredModeId; - } if (mode != null && (mode.isRefreshRateSet() || mode.isResolutionSet())) { Display.Mode matchingSupportedMode; matchingSupportedMode = findMode(mode.getPhysicalWidth(), diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 5599f2cfc374..9c95e31cc5f5 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -1032,8 +1032,12 @@ public class AppTransitionController { private void applyAnimations(ArraySet<ActivityRecord> openingApps, ArraySet<ActivityRecord> closingApps, @TransitionOldType int transit, LayoutParams animLp, boolean voiceInteraction) { + final RecentsAnimationController rac = mService.getRecentsAnimationController(); if (transit == WindowManager.TRANSIT_OLD_UNSET || (openingApps.isEmpty() && closingApps.isEmpty())) { + if (rac != null) { + rac.sendTasksAppeared(); + } return; } @@ -1071,7 +1075,6 @@ public class AppTransitionController { voiceInteraction); applyAnimations(closingWcs, closingApps, transit, false /* visible */, animLp, voiceInteraction); - final RecentsAnimationController rac = mService.getRecentsAnimationController(); if (rac != null) { rac.sendTasksAppeared(); } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index b26de07461d6..4c69f87106d1 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -2168,10 +2168,7 @@ public class DisplayPolicy { * If the decor insets changes, the display configuration may be affected. The caller should * call {@link DisplayContent#sendNewConfiguration()} if this method returns {@code true}. */ - boolean updateDecorInsetsInfoIfNeeded(WindowState win) { - if (!win.providesNonDecorInsets()) { - return false; - } + boolean updateDecorInsetsInfo() { final DisplayFrames displayFrames = mDisplayContent.mDisplayFrames; final int rotation = displayFrames.mRotation; final int dw = displayFrames.mWidth; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 383fcb9303d7..07ae167f5e66 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -477,7 +477,10 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } mLocalInsetsSourceProviders.remove(insetsTypes[i]); } - mDisplayContent.getInsetsStateController().updateAboveInsetsState(true); + // Update insets if this window is attached. + if (mDisplayContent != null) { + mDisplayContent.getInsetsStateController().updateAboveInsetsState(true); + } } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index c96253ccf70a..4d37e0816639 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1858,8 +1858,12 @@ public class WindowManagerService extends IWindowManager.Stub ProtoLog.v(WM_DEBUG_ADD_REMOVE, "addWindow: New client %s" + ": window=%s Callers=%s", client.asBinder(), win, Debug.getCallers(5)); - if ((win.isVisibleRequestedOrAdding() && displayContent.updateOrientation()) - || displayPolicy.updateDecorInsetsInfoIfNeeded(win)) { + boolean needToSendNewConfiguration = + win.isVisibleRequestedOrAdding() && displayContent.updateOrientation(); + if (win.providesNonDecorInsets()) { + needToSendNewConfiguration |= displayPolicy.updateDecorInsetsInfo(); + } + if (needToSendNewConfiguration) { displayContent.sendNewConfiguration(); } @@ -2329,8 +2333,8 @@ public class WindowManagerService extends IWindowManager.Stub & WindowManager.LayoutParams.SYSTEM_UI_VISIBILITY_CHANGED) != 0) { win.mLayoutNeeded = true; } - if (layoutChanged) { - configChanged = displayPolicy.updateDecorInsetsInfoIfNeeded(win); + if (layoutChanged && win.providesNonDecorInsets()) { + configChanged = displayPolicy.updateDecorInsetsInfo(); } if (win.mActivityRecord != null && ((flagChanges & FLAG_SHOW_WHEN_LOCKED) != 0 || (flagChanges & FLAG_DISMISS_KEYGUARD) != 0)) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 7c9d27704442..42d28612c83f 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -2627,11 +2627,19 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } } + // Check if window provides non decor insets before clearing its provided insets. + final boolean windowProvidesNonDecorInsets = providesNonDecorInsets(); + removeImmediately(); // Removing a visible window may affect the display orientation so just update it if // needed. Also recompute configuration if it provides screen decor insets. - if ((wasVisible && displayContent.updateOrientation()) - || displayContent.getDisplayPolicy().updateDecorInsetsInfoIfNeeded(this)) { + boolean needToSendNewConfiguration = wasVisible && displayContent.updateOrientation(); + if (windowProvidesNonDecorInsets) { + needToSendNewConfiguration |= + displayContent.getDisplayPolicy().updateDecorInsetsInfo(); + } + + if (needToSendNewConfiguration) { displayContent.sendNewConfiguration(); } mWmService.updateFocusedWindowLocked(isFocused() diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java index ed369c016770..9c615d140e85 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -764,13 +764,11 @@ public class LocalDisplayAdapterTest { @Test public void testGetSystemPreferredDisplayMode() throws Exception { SurfaceControl.DisplayMode displayMode1 = createFakeDisplayMode(0, 1920, 1080, 60f); - // system preferred mode + // preferred mode SurfaceControl.DisplayMode displayMode2 = createFakeDisplayMode(1, 3840, 2160, 60f); - // user preferred mode - SurfaceControl.DisplayMode displayMode3 = createFakeDisplayMode(2, 1920, 1080, 30f); SurfaceControl.DisplayMode[] modes = - new SurfaceControl.DisplayMode[]{displayMode1, displayMode2, displayMode3}; + new SurfaceControl.DisplayMode[]{displayMode1, displayMode2}; FakeDisplay display = new FakeDisplay(PORT_A, modes, 0, 1); setUpDisplay(display); updateAvailableDisplays(); @@ -782,43 +780,24 @@ public class LocalDisplayAdapterTest { DisplayDeviceInfo displayDeviceInfo = mListener.addedDisplays.get( 0).getDisplayDeviceInfoLocked(); - assertThat(displayDeviceInfo.supportedModes.length).isEqualTo(modes.length); - Display.Mode defaultMode = getModeById(displayDeviceInfo, displayDeviceInfo.defaultModeId); - assertThat(matches(defaultMode, displayMode1)).isTrue(); - // Set the user preferred display mode - mListener.addedDisplays.get(0).setUserPreferredDisplayModeLocked( - new Display.Mode( - displayMode3.width, displayMode3.height, displayMode3.refreshRate)); - updateAvailableDisplays(); - waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); - displayDeviceInfo = mListener.addedDisplays.get( - 0).getDisplayDeviceInfoLocked(); - defaultMode = getModeById(displayDeviceInfo, displayDeviceInfo.defaultModeId); - assertThat(matches(defaultMode, displayMode3)).isTrue(); + assertThat(displayDeviceInfo.supportedModes.length).isEqualTo(modes.length); - // clear the user preferred mode - mListener.addedDisplays.get(0).setUserPreferredDisplayModeLocked(null); - updateAvailableDisplays(); - waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); - displayDeviceInfo = mListener.addedDisplays.get( - 0).getDisplayDeviceInfoLocked(); - defaultMode = getModeById(displayDeviceInfo, displayDeviceInfo.defaultModeId); + Display.Mode defaultMode = getModeById(displayDeviceInfo, displayDeviceInfo.defaultModeId); assertThat(matches(defaultMode, displayMode2)).isTrue(); - // Change the display and add new system preferred mode - SurfaceControl.DisplayMode addedDisplayInfo = createFakeDisplayMode(3, 2340, 1080, 20f); - modes = new SurfaceControl.DisplayMode[]{ - displayMode1, displayMode2, displayMode3, addedDisplayInfo}; + // Change the display and add new preferred mode + SurfaceControl.DisplayMode addedDisplayInfo = createFakeDisplayMode(2, 2340, 1080, 60f); + modes = new SurfaceControl.DisplayMode[]{displayMode1, displayMode2, addedDisplayInfo}; display.dynamicInfo.supportedDisplayModes = modes; - display.dynamicInfo.preferredBootDisplayMode = 3; + display.dynamicInfo.preferredBootDisplayMode = 2; setUpDisplay(display); mInjector.getTransmitter().sendHotplug(display, /* connected */ true); waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); assertTrue(mListener.traversalRequested); assertThat(mListener.addedDisplays.size()).isEqualTo(1); - assertThat(mListener.changedDisplays.size()).isEqualTo(3); + assertThat(mListener.changedDisplays.size()).isEqualTo(1); DisplayDevice displayDevice = mListener.changedDisplays.get(0); displayDevice.applyPendingDisplayDeviceInfoChangesLocked(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 95e9f20011d0..d5447447a7b2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2992,7 +2992,8 @@ public class ActivityRecordTests extends WindowTestsBase { .setSystemDecorations(true).build(); // Add a decor insets provider window. final WindowState navbar = createNavBarWithProvidedInsets(squareDisplay); - squareDisplay.getDisplayPolicy().updateDecorInsetsInfoIfNeeded(navbar); + assertTrue(navbar.providesNonDecorInsets() + && squareDisplay.getDisplayPolicy().updateDecorInsetsInfo()); squareDisplay.sendNewConfiguration(); final Task task = new TaskBuilder(mSupervisor).setDisplay(squareDisplay).build(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index e00296f4d801..73eb1273efa3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -292,12 +292,16 @@ public class DisplayPolicyTests extends WindowTestsBase { final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy(); final DisplayInfo di = mDisplayContent.getDisplayInfo(); final int prevScreenHeightDp = mDisplayContent.getConfiguration().screenHeightDp; - assertTrue(displayPolicy.updateDecorInsetsInfoIfNeeded(navbar)); + assertTrue(navbar.providesNonDecorInsets() && displayPolicy.updateDecorInsetsInfo()); assertEquals(NAV_BAR_HEIGHT, displayPolicy.getDecorInsetsInfo(di.rotation, di.logicalWidth, di.logicalHeight).mConfigInsets.bottom); mDisplayContent.sendNewConfiguration(); assertNotEquals(prevScreenHeightDp, mDisplayContent.getConfiguration().screenHeightDp); - assertFalse(displayPolicy.updateDecorInsetsInfoIfNeeded(navbar)); + assertFalse(navbar.providesNonDecorInsets() && displayPolicy.updateDecorInsetsInfo()); + + navbar.removeIfPossible(); + assertEquals(0, displayPolicy.getDecorInsetsInfo(di.rotation, di.logicalWidth, + di.logicalHeight).mNonDecorInsets.bottom); } @UseTestDisplay(addWindows = { W_NAVIGATION_BAR, W_INPUT_METHOD }) |