diff options
141 files changed, 2659 insertions, 1106 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c57243d0bc73..2ad6669979b7 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9410,6 +9410,13 @@ public final class Settings { "even_dimmer_min_nits"; /** + * Setting that holds EM_VALUE (proprietary) + * + * @hide + */ + public static final String EM_VALUE = + "em_value"; + /** * List of the enabled print services. * * N and beyond uses {@link #DISABLED_PRINT_SERVICES}. But this might be used in an upgrade diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index 41567fbf8b51..4258ca4fde01 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -194,3 +194,14 @@ flag { is_fixed_read_only: true is_exported: true } + +flag { + name: "fallback_display_for_secondary_user_on_secondary_display" + namespace: "input_method" + description: "Feature flag to fix the fallback display bug for visible background users" + bug: "383228193" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 9fe3fd6ddc1a..7c75d7b30037 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -5820,8 +5820,13 @@ public class RemoteViews implements Parcelable, Filter { mActions.forEach(action -> { if (viewId == action.mViewId && action instanceof SetOnClickResponse setOnClickResponse) { - setOnClickResponse.mResponse.handleViewInteraction( - player, params.handler); + final RemoteResponse response = setOnClickResponse.mResponse; + if (response.mFillIntent == null) { + response.mFillIntent = new Intent(); + } + response.mFillIntent.putExtra( + "remotecompose_metadata", metadata); + response.handleViewInteraction(player, params.handler); } }); }); diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index c901ee1e3f8f..cf81ba157bf9 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -106,6 +106,7 @@ message SecureSettingsProto { optional SettingProto display_daltonizer_saturation_level = 58 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto accessibility_key_gesture_targets = 59 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto hct_rect_prompt_status = 60 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto em_value = 61 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Accessibility accessibility = 2; diff --git a/core/res/res/values/config_display.xml b/core/res/res/values/config_display.xml index c458d0e9a3c0..b6500b71e8a9 100644 --- a/core/res/res/values/config_display.xml +++ b/core/res/res/values/config_display.xml @@ -31,5 +31,8 @@ <bool name="config_evenDimmerEnabled">false</bool> <!-- Jar file path to look for PluginProvider --> <string name="config_pluginsProviderJarPath"/> - + <!-- Indicate available EM_VALUE options --> + <integer-array name="config_availableEMValueOptions"> + <item>0</item> <!-- DEFAULT --> + </integer-array> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 6ee2839788af..4789624685c2 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1309,6 +1309,7 @@ <java-symbol type="array" name="config_securityStatePackages" /> <java-symbol type="array" name="stoppable_fgs_system_apps" /> <java-symbol type="array" name="vendor_stoppable_fgs_system_apps" /> + <java-symbol type="array" name="config_availableEMValueOptions" /> <java-symbol type="drawable" name="default_wallpaper" /> <java-symbol type="drawable" name="default_lock_wallpaper" /> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 348f13a493b1..8efeecb56dbf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1449,6 +1449,15 @@ public class BubbleController implements ConfigurationChangeListener, } /** + * Expands and selects a bubble created from a running task in a different mode. + * + * @param taskInfo the task. + */ + public void expandStackAndSelectBubble(ActivityManager.RunningTaskInfo taskInfo) { + // TODO(384976265): Not implemented yet + } + + /** * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble * exists for this entry, and it is able to bubble, a new bubble will be created. * 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 d8c7f4cbb698..48b0a6cb364b 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 @@ -736,7 +736,8 @@ public abstract class WMShellModule { DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, DesktopTilingDecorViewModel desktopTilingDecorViewModel, - DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider) { + DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, + Optional<BubbleController> bubbleController) { return new DesktopTasksController( context, shellInit, @@ -768,7 +769,8 @@ public abstract class WMShellModule { desktopModeEventLogger, desktopModeUiEventLogger, desktopTilingDecorViewModel, - desktopWallpaperActivityTokenProvider); + desktopWallpaperActivityTokenProvider, + bubbleController); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 4e7cba23116c..d180ea7b79ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -73,6 +73,7 @@ import com.android.wm.shell.Flags.enableFlexibleSplit import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ExternalInterfaceBinder @@ -172,6 +173,7 @@ class DesktopTasksController( private val desktopModeUiEventLogger: DesktopModeUiEventLogger, private val desktopTilingDecorViewModel: DesktopTilingDecorViewModel, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, + private val bubbleController: Optional<BubbleController>, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, @@ -2190,6 +2192,19 @@ class DesktopTasksController( } } + /** Requests a task be transitioned from whatever mode it's in to a bubble. */ + fun requestFloat(taskInfo: RunningTaskInfo) { + val isDragging = dragToDesktopTransitionHandler.inProgress + val shouldRequestFloat = + taskInfo.isFullscreen || taskInfo.isFreeform || isDragging || taskInfo.isMultiWindow + if (!shouldRequestFloat) return + if (isDragging) { + releaseVisualIndicator() + } else { + bubbleController.ifPresent { it.expandStackAndSelectBubble(taskInfo) } + } + } + private fun getDefaultDensityDpi(): Int { return context.resources.displayMetrics.densityDpi } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt index f6ebf7221e82..6e12bf4d1dda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt @@ -90,10 +90,10 @@ constructor( Source.HEADER_BUTTON_TO_RESTORE -> Cuj.CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW Source.KEYBOARD_SHORTCUT -> null Source.HEADER_DRAG_TO_TOP -> null - Source.MAXIMIZE_MENU_TO_MAXIMIZE -> null - Source.MAXIMIZE_MENU_TO_RESTORE -> null - Source.DOUBLE_TAP_TO_MAXIMIZE -> null - Source.DOUBLE_TAP_TO_RESTORE -> null + Source.MAXIMIZE_MENU_TO_MAXIMIZE -> Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW + Source.MAXIMIZE_MENU_TO_RESTORE -> Cuj.CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW + Source.DOUBLE_TAP_TO_MAXIMIZE -> Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW + Source.DOUBLE_TAP_TO_RESTORE -> Cuj.CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW } /** The direction to which the task is being resized. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java index fd387d1811fb..37296531ee34 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -350,7 +350,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, } cancelPhysicsAnimation(); mMenuController.hideMenu(ANIM_TYPE_DISMISS, false /* resize */); - mPipScheduler.removePipAfterAnimation(); + mPipScheduler.scheduleRemovePip(); } /** Sets the movement bounds to use to constrain PIP position animations. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 4461a5c6a70c..7f673d2efc68 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -122,34 +122,26 @@ public class PipScheduler { * Schedules exit PiP via expand transition. */ public void scheduleExitPipViaExpand() { - WindowContainerTransaction wct = getExitPipViaExpandTransaction(); - if (wct != null) { - mMainExecutor.execute(() -> { + mMainExecutor.execute(() -> { + if (!mPipTransitionState.isInPip()) return; + WindowContainerTransaction wct = getExitPipViaExpandTransaction(); + if (wct != null) { mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, null /* destinationBounds */); - }); - } - } - - // TODO: Optimize this by running the animation as part of the transition - /** Runs remove PiP animation and schedules remove PiP transition after the animation ends. */ - public void removePipAfterAnimation() { - SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); - PipAlphaAnimator animator = mPipAlphaAnimatorSupplier.get(mContext, - mPipTransitionState.getPinnedTaskLeash(), tx, PipAlphaAnimator.FADE_OUT); - animator.setAnimationEndCallback(this::scheduleRemovePipImmediately); - animator.start(); + } + }); } /** Schedules remove PiP transition. */ - private void scheduleRemovePipImmediately() { - WindowContainerTransaction wct = getRemovePipTransaction(); - if (wct != null) { - mMainExecutor.execute(() -> { + public void scheduleRemovePip() { + mMainExecutor.execute(() -> { + if (!mPipTransitionState.isInPip()) return; + WindowContainerTransaction wct = getRemovePipTransaction(); + if (wct != null) { mPipTransitionController.startExitTransition(TRANSIT_REMOVE_PIP, wct, null /* destinationBounds */); - }); - } + } + }); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index acb5622b041c..2e38449d4584 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -278,7 +278,8 @@ public class PipTransition extends PipTransitionController implements } if (isRemovePipTransition(info)) { - return removePipImmediately(info, startTransaction, finishTransaction, finishCallback); + mPipTransitionState.setState(PipTransitionState.EXITING_PIP); + return startRemoveAnimation(info, startTransaction, finishTransaction, finishCallback); } return false; } @@ -668,13 +669,18 @@ public class PipTransition extends PipTransitionController implements return true; } - private boolean removePipImmediately(@NonNull TransitionInfo info, + private boolean startRemoveAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - startTransaction.apply(); - finishCallback.onTransitionFinished(null); - mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + TransitionInfo.Change pipChange = getChangeByToken(info, + mPipTransitionState.getPipTaskToken()); + mFinishCallback = finishCallback; + PipAlphaAnimator animator = new PipAlphaAnimator(mContext, pipChange.getLeash(), + startTransaction, PipAlphaAnimator.FADE_OUT); + finishTransaction.setAlpha(pipChange.getLeash(), 0f); + animator.setAnimationEndCallback(this::finishTransition); + animator.start(); return true; } 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 e80016d07f15..1689bb5778ae 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 @@ -329,7 +329,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @ColorInt int backgroundColorForTransition = 0; final int wallpaperTransit = getWallpaperTransitType(info); - boolean isDisplayRotationAnimationStarted = false; + int animatingDisplayId = Integer.MIN_VALUE; final boolean isDreamTransition = isDreamTransition(info); final boolean isOnlyTranslucent = isOnlyTranslucent(info); final boolean isActivityLevel = isActivityLevelOnly(info); @@ -361,7 +361,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { ? ScreenRotationAnimation.FLAG_HAS_WALLPAPER : 0; startRotationAnimation(startTransaction, change, info, anim, flags, animations, onAnimFinish); - isDisplayRotationAnimationStarted = true; + animatingDisplayId = change.getEndDisplayId(); continue; } } else { @@ -426,8 +426,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // Hide the invisible surface directly without animating it if there is a display // rotation animation playing. - if (isDisplayRotationAnimationStarted && TransitionUtil.isClosingType(mode)) { - startTransaction.hide(change.getLeash()); + if (animatingDisplayId == change.getEndDisplayId()) { + if (TransitionUtil.isClosingType(mode)) { + startTransaction.hide(change.getLeash()); + } + // Only need to play display level animation. continue; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index df81821ff13f..edb2e1cbcca6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -546,8 +546,13 @@ public class Transitions implements RemoteCallable<Transitions>, // When the window is moved to front, make sure the crop is updated to prevent it // from using the old crop. t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y); - t.setWindowCrop(leash, change.getEndAbsBounds().width(), - change.getEndAbsBounds().height()); + if (change.getContainer() != null) { + // We don't want to crop on non-remotable (activity), because it can have + // letterbox child surface that is position at a negative position related to + // the activity's surface. + t.setWindowCrop(leash, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); + } } // Don't move anything that isn't independent within its parents @@ -557,8 +562,13 @@ public class Transitions implements RemoteCallable<Transitions>, t.setMatrix(leash, 1, 0, 0, 1); t.setAlpha(leash, 1.f); t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y); - t.setWindowCrop(leash, change.getEndAbsBounds().width(), - change.getEndAbsBounds().height()); + if (change.getContainer() != null) { + // We don't want to crop on non-remotable (activity), because it can have + // letterbox child surface that is position at a negative position related + // to the activity's surface. + t.setWindowCrop(leash, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); + } } continue; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index a7a5f09c88f8..046cb202fb11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -776,6 +776,18 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, DesktopUiEventEnum.DESKTOP_WINDOW_APP_HANDLE_MENU_TAP_TO_SPLIT_SCREEN); } + private void onToFloat(int taskId) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + decoration.closeHandleMenu(); + // When the app enters float, the handle will no longer be visible, meaning + // we shouldn't receive input for it any longer. + decoration.disposeStatusBarInputLayer(); + mDesktopTasksController.requestFloat(decoration.mTaskInfo); + } + private void onNewWindow(int taskId) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) { @@ -1731,6 +1743,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, onToSplitScreen(taskInfo.taskId); return Unit.INSTANCE; }); + windowDecoration.setOnToFloatClickListener(() -> { + onToFloat(taskInfo.taskId); + return Unit.INSTANCE; + }); windowDecoration.setOpenInBrowserClickListener((intent) -> { onOpenInBrowser(taskInfo.taskId, intent); }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 0d1960ad6e29..4ac89546c9c7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -155,6 +155,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private Consumer<DesktopModeTransitionSource> mOnToDesktopClickListener; private Function0<Unit> mOnToFullscreenClickListener; private Function0<Unit> mOnToSplitscreenClickListener; + private Function0<Unit> mOnToFloatClickListener; private Function0<Unit> mOnNewWindowClickListener; private Function0<Unit> mOnManageWindowsClickListener; private Function0<Unit> mOnChangeAspectRatioClickListener; @@ -351,6 +352,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mOnToSplitscreenClickListener = listener; } + /** Registers a listener to be called when the decoration's to-split action is triggered. */ + void setOnToFloatClickListener(Function0<Unit> listener) { + mOnToFloatClickListener = listener; + } + /** * Adds a drag resize observer that gets notified on the task being drag resized. * @@ -1372,6 +1378,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin }, /* onToFullscreenClickListener= */ mOnToFullscreenClickListener, /* onToSplitScreenClickListener= */ mOnToSplitscreenClickListener, + /* onToFloatClickListener= */ mOnToFloatClickListener, /* onNewWindowClickListener= */ mOnNewWindowClickListener, /* onManageWindowsClickListener= */ mOnManageWindowsClickListener, /* onAspectRatioSettingsClickListener= */ mOnChangeAspectRatioClickListener, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index bb19a2cc2ad4..9d73950abcf0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -144,6 +144,7 @@ class HandleMenu( onToDesktopClickListener: () -> Unit, onToFullscreenClickListener: () -> Unit, onToSplitScreenClickListener: () -> Unit, + onToFloatClickListener: () -> Unit, onNewWindowClickListener: () -> Unit, onManageWindowsClickListener: () -> Unit, onChangeAspectRatioClickListener: () -> Unit, @@ -162,6 +163,7 @@ class HandleMenu( onToDesktopClickListener = onToDesktopClickListener, onToFullscreenClickListener = onToFullscreenClickListener, onToSplitScreenClickListener = onToSplitScreenClickListener, + onToFloatClickListener = onToFloatClickListener, onNewWindowClickListener = onNewWindowClickListener, onManageWindowsClickListener = onManageWindowsClickListener, onChangeAspectRatioClickListener = onChangeAspectRatioClickListener, @@ -183,6 +185,7 @@ class HandleMenu( onToDesktopClickListener: () -> Unit, onToFullscreenClickListener: () -> Unit, onToSplitScreenClickListener: () -> Unit, + onToFloatClickListener: () -> Unit, onNewWindowClickListener: () -> Unit, onManageWindowsClickListener: () -> Unit, onChangeAspectRatioClickListener: () -> Unit, @@ -208,6 +211,7 @@ class HandleMenu( this.onToDesktopClickListener = onToDesktopClickListener this.onToFullscreenClickListener = onToFullscreenClickListener this.onToSplitScreenClickListener = onToSplitScreenClickListener + this.onToFloatClickListener = onToFloatClickListener this.onNewWindowClickListener = onNewWindowClickListener this.onManageWindowsClickListener = onManageWindowsClickListener this.onChangeAspectRatioClickListener = onChangeAspectRatioClickListener @@ -502,6 +506,7 @@ class HandleMenu( var onToDesktopClickListener: (() -> Unit)? = null var onToFullscreenClickListener: (() -> Unit)? = null var onToSplitScreenClickListener: (() -> Unit)? = null + var onToFloatClickListener: (() -> Unit)? = null var onNewWindowClickListener: (() -> Unit)? = null var onManageWindowsClickListener: (() -> Unit)? = null var onChangeAspectRatioClickListener: (() -> Unit)? = null @@ -515,6 +520,7 @@ class HandleMenu( splitscreenBtn.setOnClickListener { onToSplitScreenClickListener?.invoke() } desktopBtn.setOnClickListener { onToDesktopClickListener?.invoke() } openInAppOrBrowserBtn.setOnClickListener { onOpenInAppOrBrowserClickListener?.invoke() } + floatingBtn.setOnClickListener { onToFloatClickListener?.invoke() } openByDefaultBtn.setOnClickListener { onOpenByDefaultClickListener?.invoke() } @@ -640,8 +646,9 @@ class HandleMenu( private fun bindWindowingPill(style: MenuStyle) { windowingPill.background.setTint(style.backgroundColor) - // TODO: Remove once implemented. - floatingBtn.visibility = View.GONE + if (!com.android.wm.shell.Flags.enableBubbleAnything()) { + floatingBtn.visibility = View.GONE + } fullscreenBtn.isSelected = taskInfo.isFullscreen fullscreenBtn.isEnabled = !taskInfo.isFullscreen diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index fe0852689ee9..e032616e7d43 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -93,6 +93,7 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.MultiInstanceHelper @@ -232,6 +233,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock private lateinit var mockToast: Toast private lateinit var mockitoSession: StaticMockitoSession @Mock private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel + @Mock private lateinit var bubbleController: BubbleController @Mock private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration @Mock private lateinit var resources: Resources @Mock @@ -383,6 +385,7 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopModeUiEventLogger, desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, + Optional.of(bubbleController), ) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java index 3fe8c109807a..a8aa25700c7e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -120,15 +120,22 @@ public class PipSchedulerTest { @Test public void scheduleExitPipViaExpand_nullTaskToken_noop() { setNullPipTaskToken(); + when(mMockPipTransitionState.isInPip()).thenReturn(true); mPipScheduler.scheduleExitPipViaExpand(); - verify(mMockMainExecutor, never()).execute(any()); + verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture()); + assertNotNull(mRunnableArgumentCaptor.getValue()); + mRunnableArgumentCaptor.getValue().run(); + + verify(mMockPipTransitionController, never()) + .startExitTransition(eq(TRANSIT_EXIT_PIP), any(), isNull()); } @Test public void scheduleExitPipViaExpand_exitTransitionCalled() { setMockPipTaskToken(); + when(mMockPipTransitionState.isInPip()).thenReturn(true); mPipScheduler.scheduleExitPipViaExpand(); @@ -142,20 +149,13 @@ public class PipSchedulerTest { @Test public void removePipAfterAnimation() { - //TODO: Update once this is changed to run animation as part of transition setMockPipTaskToken(); + when(mMockPipTransitionState.isInPip()).thenReturn(true); - mPipScheduler.removePipAfterAnimation(); - verify(mMockAlphaAnimator, times(1)) - .setAnimationEndCallback(mRunnableArgumentCaptor.capture()); - assertNotNull(mRunnableArgumentCaptor.getValue()); - verify(mMockAlphaAnimator, times(1)).start(); - - mRunnableArgumentCaptor.getValue().run(); + mPipScheduler.scheduleRemovePip(); verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture()); assertNotNull(mRunnableArgumentCaptor.getValue()); - mRunnableArgumentCaptor.getValue().run(); verify(mMockPipTransitionController, times(1)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index e99e5cce8b27..7dac0859b7e9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -1091,6 +1091,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), openInBrowserCaptor.capture(), any(), any(), @@ -1127,6 +1128,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), openInBrowserCaptor.capture(), any(), any(), @@ -1158,6 +1160,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), openInBrowserCaptor.capture(), any(), any(), @@ -1226,6 +1229,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), closeClickListener.capture(), any(), anyBoolean() @@ -1258,6 +1262,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), /* forceShowSystemBars= */ eq(true) ); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index cbfb57edc72d..f90988e90b22 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -307,6 +307,7 @@ class HandleMenuTest : ShellTestCase() { onToDesktopClickListener = mock(), onToFullscreenClickListener = mock(), onToSplitScreenClickListener = mock(), + onToFloatClickListener = mock(), onNewWindowClickListener = mock(), onManageWindowsClickListener = mock(), onChangeAspectRatioClickListener = mock(), diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java index 170cb4546d0c..551f52301b56 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java @@ -47,19 +47,19 @@ import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.StringRes; + +import com.android.packageinstaller.common.EventResultPersister; +import com.android.packageinstaller.common.UninstallEventReceiver; import com.android.packageinstaller.handheld.ErrorDialogFragment; import com.android.packageinstaller.handheld.UninstallAlertDialogFragment; import com.android.packageinstaller.television.ErrorFragment; import com.android.packageinstaller.television.UninstallAlertFragment; import com.android.packageinstaller.television.UninstallAppProgress; -import com.android.packageinstaller.common.EventResultPersister; -import com.android.packageinstaller.common.UninstallEventReceiver; import com.android.packageinstaller.v2.ui.UninstallLaunch; -import java.util.List; - /* * This activity presents UI to uninstall an application. Usually launched with intent * Intent.ACTION_UNINSTALL_PKG_COMMAND and attribute @@ -181,12 +181,15 @@ public class UninstallerActivity extends Activity { if (mDialogInfo.user == null) { mDialogInfo.user = Process.myUserHandle(); } else { - List<UserHandle> profiles = userManager.getUserProfiles(); - if (!profiles.contains(mDialogInfo.user)) { - Log.e(TAG, "User " + Process.myUserHandle() + " can't request uninstall " - + "for user " + mDialogInfo.user); - showUserIsNotAllowed(); - return; + if (mDialogInfo.user != Process.myUserHandle()) { + final boolean isCurrentUserProfileOwner = + (Process.myUserHandle() == userManager.getProfileParent(mDialogInfo.user)); + if (!isCurrentUserProfileOwner) { + Log.e(TAG, "User " + Process.myUserHandle() + " can't request uninstall " + + "for user " + mDialogInfo.user); + showUserIsNotAllowed(); + return; + } } } diff --git a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt index 620d717faf69..7432254b57a4 100644 --- a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt +++ b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt @@ -129,7 +129,15 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { ) } } - processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use { + val javaFileObject = + try { + processingEnv.filer.createSourceFile("$outputPkg.$outputClass") + } catch (e: Exception) { + // quick fix: gradle runs this processor twice unexpectedly + warn("cannot createSourceFile: $e") + return + } + javaFileObject.openWriter().use { it.write("package $outputPkg;\n\n") it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n\n") it.write("// Generated by annotation processor for @$ANNOTATION_NAME\n") diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt index c1edbdc20361..1e70a32cb38b 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt @@ -22,7 +22,6 @@ import android.os.Bundle import androidx.annotation.AnyThread import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.fragment.app.Fragment /** * Interface provides preference metadata (title, summary, icon, etc.). @@ -170,47 +169,3 @@ interface PreferenceMetadata { /** Metadata of preference category. */ @AnyThread open class PreferenceCategory(override val key: String, override val title: Int) : PreferenceGroup - -/** Metadata of preference screen. */ -@AnyThread -interface PreferenceScreenMetadata : PreferenceMetadata { - - /** - * The screen title resource, which precedes [getScreenTitle] if provided. - * - * By default, screen title is same with [title]. - */ - val screenTitle: Int - get() = title - - /** Returns dynamic screen title, use [screenTitle] whenever possible. */ - fun getScreenTitle(context: Context): CharSequence? = null - - /** Returns the fragment class to show the preference screen. */ - fun fragmentClass(): Class<out Fragment>? - - /** - * Indicates if [getPreferenceHierarchy] returns a complete hierarchy of the preference screen. - * - * If `true`, the result of [getPreferenceHierarchy] will be used to inflate preference screen. - * Otherwise, it is an intermediate state called hybrid mode, preference hierarchy is - * represented by other ways (e.g. XML resource) and [PreferenceMetadata]s in - * [getPreferenceHierarchy] will only be used to bind UI widgets. - */ - fun hasCompleteHierarchy(): Boolean = true - - /** - * Returns the hierarchy of preference screen. - * - * The implementation MUST include all preferences into the hierarchy regardless of the runtime - * conditions. DO NOT check any condition (except compile time flag) before adding a preference. - */ - fun getPreferenceHierarchy(context: Context): PreferenceHierarchy - - /** - * Returns the [Intent] to show current preference screen. - * - * @param metadata the preference to locate when show the screen - */ - fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?): Intent? = null -} diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt new file mode 100644 index 000000000000..fc68ea7f8a2f --- /dev/null +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +import android.content.Context +import android.content.Intent +import androidx.annotation.AnyThread +import androidx.fragment.app.Fragment + +/** Metadata of preference screen. */ +@AnyThread +interface PreferenceScreenMetadata : PreferenceMetadata { + + /** + * The screen title resource, which precedes [getScreenTitle] if provided. + * + * By default, screen title is same with [title]. + */ + val screenTitle: Int + get() = title + + /** Returns dynamic screen title, use [screenTitle] whenever possible. */ + fun getScreenTitle(context: Context): CharSequence? = null + + /** Returns the fragment class to show the preference screen. */ + fun fragmentClass(): Class<out Fragment>? + + /** + * Indicates if [getPreferenceHierarchy] returns a complete hierarchy of the preference screen. + * + * If `true`, the result of [getPreferenceHierarchy] will be used to inflate preference screen. + * Otherwise, it is an intermediate state called hybrid mode, preference hierarchy is + * represented by other ways (e.g. XML resource) and [PreferenceMetadata]s in + * [getPreferenceHierarchy] will only be used to bind UI widgets. + */ + fun hasCompleteHierarchy(): Boolean = true + + /** + * Returns the hierarchy of preference screen. + * + * The implementation MUST include all preferences into the hierarchy regardless of the runtime + * conditions. DO NOT check any condition (except compile time flag) before adding a preference. + */ + fun getPreferenceHierarchy(context: Context): PreferenceHierarchy + + /** + * Returns the [Intent] to show current preference screen. + * + * @param metadata the preference to locate when show the screen + */ + fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?): Intent? = null +} diff --git a/packages/SettingsLib/SliderPreference/src/com/android/settingslib/widget/SliderPreference.java b/packages/SettingsLib/SliderPreference/src/com/android/settingslib/widget/SliderPreference.java index 1815d040bb18..4315238ad7c1 100644 --- a/packages/SettingsLib/SliderPreference/src/com/android/settingslib/widget/SliderPreference.java +++ b/packages/SettingsLib/SliderPreference/src/com/android/settingslib/widget/SliderPreference.java @@ -268,7 +268,9 @@ public class SliderPreference extends Preference { mSlider.setValueFrom(mMin); mSlider.setValueTo(mMax); mSlider.setValue(mSliderValue); + mSlider.clearOnSliderTouchListeners(); mSlider.addOnSliderTouchListener(mTouchListener); + mSlider.clearOnChangeListeners(); mSlider.addOnChangeListener(mChangeListener); mSlider.setEnabled(isEnabled()); @@ -487,7 +489,7 @@ public class SliderPreference extends Preference { * set the {@link Slider}'s value to the stored value. */ void syncValueInternal(@NonNull Slider slider) { - int sliderValue = mMin + (int) slider.getValue(); + int sliderValue = (int) slider.getValue(); if (sliderValue != mSliderValue) { if (callChangeListener(sliderValue)) { setValueInternal(sliderValue, false); diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt index fcaedd2b80bd..a52222ba678b 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt @@ -64,7 +64,7 @@ object SettingsDimension { bottom = itemPaddingVertical, ) val itemPaddingAround = 8.dp - val itemDividerHeight = 32.dp + val itemDividerHeight = if (isSpaExpressiveEnabled) 40.dp else 32.dp val iconLarge = 48.dp val introIconSize = 40.dp diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt index 3aa7ad0a992d..8b1ed939a967 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt @@ -28,8 +28,6 @@ import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -104,8 +102,17 @@ internal fun TwoTargetPreference( @Composable private fun PreferenceDivider() { Box( - Modifier.padding(horizontal = SettingsDimension.itemPaddingEnd) - .size(width = 1.dp, height = SettingsDimension.itemDividerHeight) - .background(color = MaterialTheme.colorScheme.divider) + if (isSpaExpressiveEnabled) { + Modifier.padding( + start = SettingsDimension.paddingSmall, + end = SettingsDimension.paddingExtraSmall6, + ) + .size(width = 1.dp, height = SettingsDimension.itemDividerHeight) + .background(color = MaterialTheme.colorScheme.outline) + } else { + Modifier.padding(horizontal = SettingsDimension.itemPaddingEnd) + .size(width = 1.dp, height = SettingsDimension.itemDividerHeight) + .background(color = MaterialTheme.colorScheme.divider) + } ) } diff --git a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/FlowTestUtil.kt b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/FlowTestUtil.kt index 99c6a3fd3465..591dff7af5a0 100644 --- a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/FlowTestUtil.kt +++ b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/FlowTestUtil.kt @@ -17,18 +17,50 @@ package com.android.settingslib.spa.testutils import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withTimeoutOrNull +/** + * Collects the first element emitted by this flow within a given timeout. + * + * If the flow emits a value within the given timeout, this function returns that value. If the + * timeout expires before the flow emits any values, this function returns null. + * + * This function is similar to [kotlinx.coroutines.flow.firstOrNull], but it adds a timeout to + * prevent potentially infinite waiting. + * + * @param timeMillis The timeout in milliseconds. Defaults to 500 milliseconds. + * @return The first element emitted by the flow within the timeout, or null if the timeout expires. + */ suspend fun <T> Flow<T>.firstWithTimeoutOrNull(timeMillis: Long = 500): T? = - withTimeoutOrNull(timeMillis) { - first() - } + withTimeoutOrNull(timeMillis) { firstOrNull() } -suspend fun <T> Flow<T>.toListWithTimeout(timeMillis: Long = 500): List<T> { - val list = mutableListOf<T>() - return withTimeoutOrNull(timeMillis) { - toList(list) - } ?: list +/** + * Collects elements from this flow for a given time and returns the last emitted element, or null + * if the flow did not emit any elements. + * + * This function is useful when you need to retrieve the last value emitted by a flow within a + * specific timeframe, but the flow might complete without emitting anything or might not emit a + * value within the given timeout. + * + * @param timeMillis The timeout in milliseconds. Defaults to 500ms. + * @return The last emitted element, or null if the flow did not emit any elements. + */ +suspend fun <T> Flow<T>.lastWithTimeoutOrNull(timeMillis: Long = 500): T? = + toListWithTimeout(timeMillis).lastOrNull() + +/** + * Collects elements from this flow into a list with a timeout. + * + * This function attempts to collect all elements from the flow and store them in a list. If the + * collection process takes longer than the specified timeout, the collection is canceled and the + * function returns the elements collected up to that point. + * + * @param timeMillis The timeout duration in milliseconds. Defaults to 500 milliseconds. + * @return A list containing the collected elements, or an empty list if the timeout was reached + * before any elements were collected. + */ +suspend fun <T> Flow<T>.toListWithTimeout(timeMillis: Long = 500): List<T> = buildList { + withTimeoutOrNull(timeMillis) { toList(this@buildList) } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt index 7d6ee1928111..60ef73317316 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt @@ -21,9 +21,8 @@ import android.content.Intent import android.content.IntentFilter import android.os.UserHandle import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverAsUserFlow /** @@ -35,6 +34,8 @@ fun DisposableBroadcastReceiverAsUser( userHandle: UserHandle, onReceive: (Intent) -> Unit, ) { - LocalContext.current.broadcastReceiverAsUserFlow(intentFilter, userHandle) - .collectLatestWithLifecycle(LocalLifecycleOwner.current, action = onReceive) + val context = LocalContext.current + LaunchedEffect(Unit) { + context.broadcastReceiverAsUserFlow(intentFilter, userHandle).collect(onReceive) + } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt index a59a724d9346..4221f9fb5111 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt @@ -22,10 +22,11 @@ import android.content.Intent import android.content.IntentFilter import android.os.UserHandle import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.junit4.createComposeRule -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Rule import org.junit.Test @@ -38,34 +39,39 @@ import org.mockito.kotlin.mock @RunWith(AndroidJUnit4::class) class DisposableBroadcastReceiverAsUserTest { - @get:Rule - val composeTestRule = createComposeRule() + @get:Rule val composeTestRule = createComposeRule() private var registeredBroadcastReceiver: BroadcastReceiver? = null - private val context = mock<Context> { - on { - registerReceiverAsUser( - any(), - eq(USER_HANDLE), - eq(INTENT_FILTER), - isNull(), - isNull(), - eq(Context.RECEIVER_NOT_EXPORTED), - ) - } doAnswer { - registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver - null + private val context = + mock<Context> { + on { + registerReceiverAsUser( + any(), + eq(USER_HANDLE), + eq(INTENT_FILTER), + isNull(), + isNull(), + eq(Context.RECEIVER_NOT_EXPORTED), + ) + } doAnswer + { + registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver + null + } + + on { unregisterReceiver(any()) } doAnswer + { + if (registeredBroadcastReceiver === it.arguments[0]) { + registeredBroadcastReceiver = null + } + } } - } @Test fun broadcastReceiver_registered() { composeTestRule.setContent { - CompositionLocalProvider( - LocalContext provides context, - LocalLifecycleOwner provides TestLifecycleOwner(), - ) { + CompositionLocalProvider(LocalContext provides context) { DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {} } } @@ -77,10 +83,7 @@ class DisposableBroadcastReceiverAsUserTest { fun broadcastReceiver_isCalledOnReceive() { var onReceiveIsCalled = false composeTestRule.setContent { - CompositionLocalProvider( - LocalContext provides context, - LocalLifecycleOwner provides TestLifecycleOwner(), - ) { + CompositionLocalProvider(LocalContext provides context) { DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) { onReceiveIsCalled = true } @@ -92,6 +95,23 @@ class DisposableBroadcastReceiverAsUserTest { composeTestRule.waitUntil { onReceiveIsCalled } } + @Test + fun broadcastReceiver_unregistered() { + var isBroadcastReceiverRegistered by mutableStateOf(true) + composeTestRule.setContent { + if (isBroadcastReceiverRegistered) { + CompositionLocalProvider(LocalContext provides context) { + DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {} + } + } + } + composeTestRule.waitUntil { registeredBroadcastReceiver != null } + + isBroadcastReceiverRegistered = false + + composeTestRule.waitUntil { registeredBroadcastReceiver == null } + } + private companion object { val USER_HANDLE: UserHandle = UserHandle.of(0) diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt index 31522c1209f7..7ef11eb865ba 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt @@ -23,7 +23,6 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull import com.android.settingslib.spa.testutils.toListWithTimeout -import com.android.settingslib.spaprivileged.framework.common.asUser import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -35,6 +34,7 @@ import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class PermissionsChangedFlowTest { @@ -49,7 +49,7 @@ class PermissionsChangedFlowTest { } private val context: Context = spy(ApplicationProvider.getApplicationContext()) { - on { asUser(APP.userHandle) } doReturn mock + doReturn(mock).whenever(mock).createContextAsUser(APP.userHandle, 0) on { packageManager } doReturn mockPackageManager } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java index ca0cad759a08..7d91050d4289 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java @@ -19,6 +19,7 @@ package com.android.settingslib.bluetooth; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; +import android.media.MediaRecorder; import androidx.annotation.IntDef; @@ -61,15 +62,20 @@ public final class HearingAidAudioRoutingConstants { @IntDef({ RoutingValue.AUTO, RoutingValue.HEARING_DEVICE, - RoutingValue.DEVICE_SPEAKER, + RoutingValue.BUILTIN_DEVICE, }) public @interface RoutingValue { int AUTO = 0; int HEARING_DEVICE = 1; - int DEVICE_SPEAKER = 2; + int BUILTIN_DEVICE = 2; } - public static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes( + public static final AudioDeviceAttributes BUILTIN_SPEAKER = new AudioDeviceAttributes( AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, ""); + public static final AudioDeviceAttributes BUILTIN_MIC = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_BUILTIN_MIC, ""); + + public static final int MICROPHONE_SOURCE_VOICE_COMMUNICATION = + MediaRecorder.AudioSource.VOICE_COMMUNICATION; } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java index 8eaea0e3561b..1f727259db7e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java @@ -16,26 +16,34 @@ package com.android.settingslib.bluetooth; +import static com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.BUILTIN_MIC; +import static com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.MICROPHONE_SOURCE_VOICE_COMMUNICATION; + import android.content.Context; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.audiopolicy.AudioProductStrategy; +import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.RoutingValue; + import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** - * A helper class to configure the routing strategy for hearing aids. + * A helper class to configure the audio routing for hearing aids. */ public class HearingAidAudioRoutingHelper { + private static final String TAG = "HearingAidAudioRoutingHelper"; + private final AudioManager mAudioManager; public HearingAidAudioRoutingHelper(Context context) { @@ -73,26 +81,26 @@ public class HearingAidAudioRoutingHelper { * @param hearingDevice {@link AudioDeviceAttributes} of the device to be changed in audio * routing * @param routingValue one of value defined in - * {@link HearingAidAudioRoutingConstants.RoutingValue}, denotes routing + * {@link RoutingValue}, denotes routing * destination. * @return {code true} if the routing value successfully configure */ public boolean setPreferredDeviceRoutingStrategies( List<AudioProductStrategy> supportedStrategies, AudioDeviceAttributes hearingDevice, - @HearingAidAudioRoutingConstants.RoutingValue int routingValue) { + @RoutingValue int routingValue) { boolean status; switch (routingValue) { - case HearingAidAudioRoutingConstants.RoutingValue.AUTO: + case RoutingValue.AUTO: status = removePreferredDeviceForStrategies(supportedStrategies); return status; - case HearingAidAudioRoutingConstants.RoutingValue.HEARING_DEVICE: + case RoutingValue.HEARING_DEVICE: status = removePreferredDeviceForStrategies(supportedStrategies); status &= setPreferredDeviceForStrategies(supportedStrategies, hearingDevice); return status; - case HearingAidAudioRoutingConstants.RoutingValue.DEVICE_SPEAKER: + case RoutingValue.BUILTIN_DEVICE: status = removePreferredDeviceForStrategies(supportedStrategies); status &= setPreferredDeviceForStrategies(supportedStrategies, - HearingAidAudioRoutingConstants.DEVICE_SPEAKER_OUT); + HearingAidAudioRoutingConstants.BUILTIN_SPEAKER); return status; default: throw new IllegalArgumentException("Unexpected routingValue: " + routingValue); @@ -100,21 +108,76 @@ public class HearingAidAudioRoutingHelper { } /** - * Gets the matched hearing device {@link AudioDeviceAttributes} for {@code device}. + * Set the preferred input device for calls. * - * <p>Will also try to match the {@link CachedBluetoothDevice#getSubDevice()} of {@code device} + * <p>Note that hearing device needs to be valid input device to be found in AudioManager. + * <p>Routing value can be: + * <ul> + * <li> {@link RoutingValue#AUTO} - Allow the system to automatically select the appropriate + * audio routing for calls.</li> + * <li> {@link RoutingValue#HEARING_DEVICE} - Set input device to this hearing device.</li> + * <li> {@link RoutingValue#BUILTIN_DEVICE} - Set input device to builtin microphone. </li> + * </ul> + * @param routingValue The desired routing value for calls + * @return {@code true} if the operation was successful + */ + public boolean setPreferredInputDeviceForCalls(@Nullable CachedBluetoothDevice hearingDevice, + @RoutingValue int routingValue) { + AudioDeviceAttributes hearingDeviceAttributes = getMatchedHearingDeviceAttributesInput( + hearingDevice); + if (hearingDeviceAttributes == null) { + Log.w(TAG, "Can not find expected input AudioDeviceAttributes for hearing device: " + + hearingDevice.getDevice().getAnonymizedAddress()); + return false; + } + + final int audioSource = MICROPHONE_SOURCE_VOICE_COMMUNICATION; + return switch (routingValue) { + case RoutingValue.AUTO -> + mAudioManager.clearPreferredDevicesForCapturePreset(audioSource); + case RoutingValue.HEARING_DEVICE -> { + mAudioManager.clearPreferredDevicesForCapturePreset(audioSource); + yield mAudioManager.setPreferredDeviceForCapturePreset(audioSource, + hearingDeviceAttributes); + } + case RoutingValue.BUILTIN_DEVICE -> { + mAudioManager.clearPreferredDevicesForCapturePreset(audioSource); + yield mAudioManager.setPreferredDeviceForCapturePreset(audioSource, BUILTIN_MIC); + } + default -> throw new IllegalArgumentException( + "Unexpected routingValue: " + routingValue); + }; + } + + /** + * Clears the preferred input device for calls. + * + * {@code true} if the operation was successful + */ + public boolean clearPreferredInputDeviceForCalls() { + return mAudioManager.clearPreferredDevicesForCapturePreset( + MICROPHONE_SOURCE_VOICE_COMMUNICATION); + } + + /** + * Gets the matched output hearing device {@link AudioDeviceAttributes} for {@code device}. + * + * <p>Will also try to match the {@link CachedBluetoothDevice#getSubDevice()} and + * {@link CachedBluetoothDevice#getMemberDevice()} of {@code device} * * @param device the {@link CachedBluetoothDevice} need to be hearing aid device * @return the requested AudioDeviceAttributes or {@code null} if not match */ @Nullable - public AudioDeviceAttributes getMatchedHearingDeviceAttributes(CachedBluetoothDevice device) { + public AudioDeviceAttributes getMatchedHearingDeviceAttributesForOutput( + @Nullable CachedBluetoothDevice device) { if (device == null || !device.isHearingAidDevice()) { return null; } AudioDeviceInfo[] audioDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); for (AudioDeviceInfo audioDevice : audioDevices) { + //TODO: b/370812132 - Need to update if TYPE_LEA_HEARING_AID is added // ASHA for TYPE_HEARING_AID, HAP for TYPE_BLE_HEADSET if (audioDevice.getType() == AudioDeviceInfo.TYPE_HEARING_AID || audioDevice.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) { @@ -126,6 +189,35 @@ public class HearingAidAudioRoutingHelper { return null; } + /** + * Gets the matched input hearing device {@link AudioDeviceAttributes} for {@code device}. + * + * <p>Will also try to match the {@link CachedBluetoothDevice#getSubDevice()} and + * {@link CachedBluetoothDevice#getMemberDevice()} of {@code device} + * + * @param device the {@link CachedBluetoothDevice} need to be hearing aid device + * @return the requested AudioDeviceAttributes or {@code null} if not match + */ + @Nullable + private AudioDeviceAttributes getMatchedHearingDeviceAttributesInput( + @Nullable CachedBluetoothDevice device) { + if (device == null || !device.isHearingAidDevice()) { + return null; + } + + AudioDeviceInfo[] audioDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); + for (AudioDeviceInfo audioDevice : audioDevices) { + //TODO: b/370812132 - Need to update if TYPE_LEA_HEARING_AID is added + // HAP for TYPE_BLE_HEADSET + if (audioDevice.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) { + if (matchAddress(device, audioDevice)) { + return new AudioDeviceAttributes(audioDevice); + } + } + } + return null; + } + private boolean matchAddress(CachedBluetoothDevice device, AudioDeviceInfo audioDevice) { final String audioDeviceAddress = audioDevice.getAddress(); final CachedBluetoothDevice subDevice = device.getSubDevice(); @@ -142,7 +234,6 @@ public class HearingAidAudioRoutingHelper { boolean status = true; for (AudioProductStrategy strategy : strategies) { status &= mAudioManager.setPreferredDeviceForStrategy(strategy, audioDevice); - } return status; diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index 1ca4c2b39a70..ad34e837f508 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -31,6 +31,7 @@ import android.util.FeatureFlagUtils; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.RoutingValue; import java.util.HashSet; import java.util.List; @@ -277,13 +278,19 @@ public class HearingAidDeviceManager { void onActiveDeviceChanged(CachedBluetoothDevice device) { if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) { - if (device.isActiveDevice(BluetoothProfile.HEARING_AID) || device.isActiveDevice( - BluetoothProfile.LE_AUDIO)) { + if (device.isConnectedHearingAidDevice()) { setAudioRoutingConfig(device); } else { clearAudioRoutingConfig(); } } + if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { + if (device.isConnectedHearingAidDevice()) { + setMicrophoneForCalls(device); + } else { + clearMicrophoneForCalls(); + } + } } void syncDeviceIfNeeded(CachedBluetoothDevice device) { @@ -311,9 +318,25 @@ public class HearingAidDeviceManager { HearingDeviceLocalDataManager.clear(mContext, device.getDevice()); } + private void setMicrophoneForCalls(CachedBluetoothDevice device) { + boolean useRemoteMicrophone = device.getDevice().isMicrophonePreferredForCalls(); + boolean status = mRoutingHelper.setPreferredInputDeviceForCalls(device, + useRemoteMicrophone ? RoutingValue.AUTO : RoutingValue.BUILTIN_DEVICE); + if (!status) { + Log.d(TAG, "Fail to configure setPreferredInputDeviceForCalls"); + } + } + + private void clearMicrophoneForCalls() { + boolean status = mRoutingHelper.clearPreferredInputDeviceForCalls(); + if (!status) { + Log.d(TAG, "Fail to configure clearMicrophoneForCalls"); + } + } + private void setAudioRoutingConfig(CachedBluetoothDevice device) { AudioDeviceAttributes hearingDeviceAttributes = - mRoutingHelper.getMatchedHearingDeviceAttributes(device); + mRoutingHelper.getMatchedHearingDeviceAttributesForOutput(device); if (hearingDeviceAttributes == null) { Log.w(TAG, "Can not find expected AudioDeviceAttributes for hearing device: " + device.getDevice().getAnonymizedAddress()); @@ -321,17 +344,13 @@ public class HearingAidDeviceManager { } final int callRoutingValue = Settings.Secure.getInt(mContentResolver, - Settings.Secure.HEARING_AID_CALL_ROUTING, - HearingAidAudioRoutingConstants.RoutingValue.AUTO); + Settings.Secure.HEARING_AID_CALL_ROUTING, RoutingValue.AUTO); final int mediaRoutingValue = Settings.Secure.getInt(mContentResolver, - Settings.Secure.HEARING_AID_MEDIA_ROUTING, - HearingAidAudioRoutingConstants.RoutingValue.AUTO); + Settings.Secure.HEARING_AID_MEDIA_ROUTING, RoutingValue.AUTO); final int ringtoneRoutingValue = Settings.Secure.getInt(mContentResolver, - Settings.Secure.HEARING_AID_RINGTONE_ROUTING, - HearingAidAudioRoutingConstants.RoutingValue.AUTO); + Settings.Secure.HEARING_AID_RINGTONE_ROUTING, RoutingValue.AUTO); final int systemSoundsRoutingValue = Settings.Secure.getInt(mContentResolver, - Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING, - HearingAidAudioRoutingConstants.RoutingValue.AUTO); + Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING, RoutingValue.AUTO); setPreferredDeviceRoutingStrategies( HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES, @@ -351,21 +370,21 @@ public class HearingAidDeviceManager { // Don't need to pass hearingDevice when we want to reset it (set to AUTO). setPreferredDeviceRoutingStrategies( HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES, - /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); + /* hearingDevice = */ null, RoutingValue.AUTO); setPreferredDeviceRoutingStrategies( HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES, - /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); + /* hearingDevice = */ null, RoutingValue.AUTO); setPreferredDeviceRoutingStrategies( HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES, - /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); + /* hearingDevice = */ null, RoutingValue.AUTO); setPreferredDeviceRoutingStrategies( HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES, - /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); + /* hearingDevice = */ null, RoutingValue.AUTO); } private void setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList, AudioDeviceAttributes hearingDevice, - @HearingAidAudioRoutingConstants.RoutingValue int routingValue) { + @RoutingValue int routingValue) { final List<AudioProductStrategy> supportedStrategies = mRoutingHelper.getSupportedStrategies(attributeSdkUsageList); diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java index 714f9519f378..f6acac17e6fa 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java @@ -18,8 +18,6 @@ package com.android.settingslib.mobile.dataservice; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; -import android.telephony.UiccPortInfo; -import android.telephony.UiccSlotMapping; public class DataServiceUtils { @@ -43,35 +41,6 @@ public class DataServiceUtils { * {@see MobileNetworkUtils#isMobileDataEnabled(Context)}. */ public static final String COLUMN_IS_MOBILE_DATA_ENABLED = "isMobileDataEnabled"; - - /** - * The name of the show toggle for physicalSim state column, - * {@see SubscriptionUtil#showToggleForPhysicalSim(SubscriptionManager)}. - */ - public static final String COLUMN_SHOW_TOGGLE_FOR_PHYSICAL_SIM = "showToggleForPhysicalSim"; - } - - /** - * Represents columns of the UiccInfoData table, define these columns from - * {@link android.telephony.UiccSlotInfo}, {@link android.telephony.UiccCardInfo}, - * {@link UiccSlotMapping} and {@link android.telephony.UiccPortInfo}.If columns of these 4 - * classes are changed, we should also update the table except PII data. - */ - public static final class UiccInfoData { - - /** The name of the UiccInfoData table. */ - public static final String TABLE_NAME = "uiccInfo"; - - /** - * The name of the ID column, set the {@link SubscriptionInfo#getSubscriptionId()} - * as the primary key. - */ - public static final String COLUMN_ID = "sudId"; - - /** - * The name of the active state column, see {@link UiccPortInfo#isActive()}. - */ - public static final String COLUMN_IS_ACTIVE = "isActive"; } /** @@ -139,12 +108,5 @@ public class DataServiceUtils { * {@link SubscriptionManager#isActiveSubscriptionId(int)}. */ public static final String COLUMN_IS_ACTIVE_SUBSCRIPTION_ID = "isActiveSubscription"; - - /** - * The name of the active data subscription state column, see - * {@link SubscriptionManager#getActiveDataSubscriptionId()}. - */ - public static final String COLUMN_IS_ACTIVE_DATA_SUBSCRIPTION = - "isActiveDataSubscriptionId"; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java index 5f7fa278082b..63573825df02 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java @@ -27,7 +27,7 @@ import androidx.room.RoomDatabase; import java.util.List; import java.util.Objects; -@Database(entities = {SubscriptionInfoEntity.class, UiccInfoEntity.class, +@Database(entities = {SubscriptionInfoEntity.class, MobileNetworkInfoEntity.class}, exportSchema = false, version = 1) public abstract class MobileNetworkDatabase extends RoomDatabase { @@ -35,8 +35,6 @@ public abstract class MobileNetworkDatabase extends RoomDatabase { public abstract SubscriptionInfoDao mSubscriptionInfoDao(); - public abstract UiccInfoDao mUiccInfoDao(); - public abstract MobileNetworkInfoDao mMobileNetworkInfoDao(); private static MobileNetworkDatabase sInstance; @@ -73,16 +71,6 @@ public abstract class MobileNetworkDatabase extends RoomDatabase { } /** - * Insert the UICC info to the UiccInfoEntity table. - * - * @param uiccInfoEntity The uiccInfoEntity. - */ - public void insertUiccInfo(UiccInfoEntity... uiccInfoEntity) { - Log.d(TAG, "insertUiccInfo"); - mUiccInfoDao().insertUiccInfo(uiccInfoEntity); - } - - /** * Insert the mobileNetwork info to the MobileNetworkInfoEntity table. * * @param mobileNetworkInfoEntity The mobileNetworkInfoEntity. @@ -100,14 +88,6 @@ public abstract class MobileNetworkDatabase extends RoomDatabase { } /** - * Query the subscription info by the subscription ID from the SubscriptionInfoEntity - * table. - */ - public SubscriptionInfoEntity querySubInfoById(String id) { - return mSubscriptionInfoDao().querySubInfoById(id); - } - - /** * Query all mobileNetwork infos from the MobileNetworkInfoEntity * table. */ @@ -116,21 +96,6 @@ public abstract class MobileNetworkDatabase extends RoomDatabase { } /** - * Query the mobileNetwork info by the subscription ID from the MobileNetworkInfoEntity - * table. - */ - public MobileNetworkInfoEntity queryMobileNetworkInfoById(String id) { - return mMobileNetworkInfoDao().queryMobileNetworkInfoBySubId(id); - } - - /** - * Query all UICC infos from the UiccInfoEntity table. - */ - public LiveData<List<UiccInfoEntity>> queryAllUiccInfo() { - return mUiccInfoDao().queryAllUiccInfos(); - } - - /** * Delete the subscriptionInfo info by the subscription ID from the SubscriptionInfoEntity * table. */ @@ -145,11 +110,4 @@ public abstract class MobileNetworkDatabase extends RoomDatabase { public void deleteMobileNetworkInfoBySubId(String id) { mMobileNetworkInfoDao().deleteBySubId(id); } - - /** - * Delete the UICC info by the subscription ID from the UiccInfoEntity table. - */ - public void deleteUiccInfoBySubId(String id) { - mUiccInfoDao().deleteBySubId(id); - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java index 13f99e9387de..6366708cfb17 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java @@ -26,11 +26,9 @@ import androidx.room.PrimaryKey; @Entity(tableName = DataServiceUtils.MobileNetworkInfoData.TABLE_NAME) public class MobileNetworkInfoEntity { - public MobileNetworkInfoEntity(@NonNull String subId, boolean isMobileDataEnabled, - boolean showToggleForPhysicalSim) { + public MobileNetworkInfoEntity(@NonNull String subId, boolean isMobileDataEnabled) { this.subId = subId; this.isMobileDataEnabled = isMobileDataEnabled; - this.showToggleForPhysicalSim = showToggleForPhysicalSim; } @PrimaryKey @@ -41,15 +39,11 @@ public class MobileNetworkInfoEntity { @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_MOBILE_DATA_ENABLED) public boolean isMobileDataEnabled; - @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_SHOW_TOGGLE_FOR_PHYSICAL_SIM) - public boolean showToggleForPhysicalSim; - @Override public int hashCode() { int result = 17; result = 31 * result + subId.hashCode(); result = 31 * result + Boolean.hashCode(isMobileDataEnabled); - result = 31 * result + Boolean.hashCode(showToggleForPhysicalSim); return result; } @@ -64,8 +58,7 @@ public class MobileNetworkInfoEntity { MobileNetworkInfoEntity info = (MobileNetworkInfoEntity) obj; return TextUtils.equals(subId, info.subId) - && isMobileDataEnabled == info.isMobileDataEnabled - && showToggleForPhysicalSim == info.showToggleForPhysicalSim; + && isMobileDataEnabled == info.isMobileDataEnabled; } public String toString() { @@ -74,8 +67,6 @@ public class MobileNetworkInfoEntity { .append(subId) .append(", isMobileDataEnabled = ") .append(isMobileDataEnabled) - .append(", activeNetworkIsCellular = ") - .append(showToggleForPhysicalSim) .append(")}"); return builder.toString(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java index 88e6a57bf45b..ff0f27b05cd1 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java @@ -30,7 +30,7 @@ public class SubscriptionInfoEntity { public SubscriptionInfoEntity(@NonNull String subId, int simSlotIndex, boolean isEmbedded, boolean isOpportunistic, String uniqueName, boolean isSubscriptionVisible, boolean isDefaultSubscriptionSelection, boolean isValidSubscription, - boolean isActiveSubscriptionId, boolean isActiveDataSubscriptionId) { + boolean isActiveSubscriptionId) { this.subId = subId; this.simSlotIndex = simSlotIndex; this.isEmbedded = isEmbedded; @@ -40,7 +40,6 @@ public class SubscriptionInfoEntity { this.isDefaultSubscriptionSelection = isDefaultSubscriptionSelection; this.isValidSubscription = isValidSubscription; this.isActiveSubscriptionId = isActiveSubscriptionId; - this.isActiveDataSubscriptionId = isActiveDataSubscriptionId; } @PrimaryKey @@ -73,17 +72,10 @@ public class SubscriptionInfoEntity { @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_SUBSCRIPTION_ID) public boolean isActiveSubscriptionId; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_DATA_SUBSCRIPTION) - public boolean isActiveDataSubscriptionId; - public int getSubId() { return Integer.valueOf(subId); } - public CharSequence getUniqueDisplayName() { - return uniqueName; - } - public boolean isActiveSubscription() { return isActiveSubscriptionId; } @@ -103,8 +95,7 @@ public class SubscriptionInfoEntity { isSubscriptionVisible, isDefaultSubscriptionSelection, isValidSubscription, - isActiveSubscriptionId, - isActiveDataSubscriptionId); + isActiveSubscriptionId); } @Override @@ -125,8 +116,7 @@ public class SubscriptionInfoEntity { && isSubscriptionVisible == info.isSubscriptionVisible && isDefaultSubscriptionSelection == info.isDefaultSubscriptionSelection && isValidSubscription == info.isValidSubscription - && isActiveSubscriptionId == info.isActiveSubscriptionId - && isActiveDataSubscriptionId == info.isActiveDataSubscriptionId; + && isActiveSubscriptionId == info.isActiveSubscriptionId; } public String toString() { @@ -149,8 +139,6 @@ public class SubscriptionInfoEntity { .append(isValidSubscription) .append(", isActiveSubscriptionId = ") .append(isActiveSubscriptionId) - .append(", isActiveDataSubscriptionId = ") - .append(isActiveDataSubscriptionId) .append(")}"); return builder.toString(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java deleted file mode 100644 index 90e5189fdf1d..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.mobile.dataservice; - -import androidx.lifecycle.LiveData; -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; - -import java.util.List; - -@Dao -public interface UiccInfoDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - void insertUiccInfo(UiccInfoEntity... uiccInfo); - - @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " ORDER BY " - + DataServiceUtils.UiccInfoData.COLUMN_ID) - LiveData<List<UiccInfoEntity>> queryAllUiccInfos(); - - @Query("SELECT COUNT(*) FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME) - int count(); - - @Query("DELETE FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE " - + DataServiceUtils.UiccInfoData.COLUMN_ID + " = :id") - void deleteBySubId(String id); -} diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java deleted file mode 100644 index 0f80edf52d80..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.mobile.dataservice; - -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.PrimaryKey; - -@Entity(tableName = DataServiceUtils.UiccInfoData.TABLE_NAME) -public class UiccInfoEntity { - - public UiccInfoEntity(@NonNull String subId, boolean isActive) { - this.subId = subId; - this.isActive = isActive; - } - - @PrimaryKey - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_ID, index = true) - @NonNull - public String subId; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_ACTIVE) - public boolean isActive; - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + subId.hashCode(); - result = 31 * result + Boolean.hashCode(isActive); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof UiccInfoEntity)) { - return false; - } - - UiccInfoEntity info = (UiccInfoEntity) obj; - return TextUtils.equals(subId, info.subId) && isActive == info.isActive; - } - - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(" {UiccInfoEntity(subId = ") - .append(subId) - .append(", isActive = ") - .append(isActive) - .append(")}"); - return builder.toString(); - } -} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java index c83524462b15..dc609bd2fa93 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java @@ -16,9 +16,14 @@ package com.android.settingslib.bluetooth; +import static com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.BUILTIN_MIC; +import static com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.BUILTIN_SPEAKER; +import static com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.MICROPHONE_SOURCE_VOICE_COMMUNICATION; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; @@ -35,10 +40,13 @@ import android.media.audiopolicy.AudioProductStrategy; import androidx.test.core.app.ApplicationProvider; +import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.RoutingValue; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.MockitoJUnit; @@ -67,28 +75,35 @@ public class HearingAidAudioRoutingHelperTest { @Spy private AudioManager mAudioManager = mContext.getSystemService(AudioManager.class); @Mock - private AudioDeviceInfo mAudioDeviceInfo; + private AudioDeviceInfo mHearingDeviceInfoOutput; + @Mock + private AudioDeviceInfo mLeHearingDeviceInfoInput; @Mock private CachedBluetoothDevice mCachedBluetoothDevice; @Mock private CachedBluetoothDevice mSubCachedBluetoothDevice; - private AudioDeviceAttributes mHearingDeviceAttribute; + private AudioDeviceAttributes mHearingDeviceAttributeOutput; private HearingAidAudioRoutingHelper mHelper; @Before public void setUp() { doReturn(mAudioManager).when(mContext).getSystemService(AudioManager.class); - when(mAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HEARING_AID); - when(mAudioDeviceInfo.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); + when(mHearingDeviceInfoOutput.getType()).thenReturn(AudioDeviceInfo.TYPE_HEARING_AID); + when(mHearingDeviceInfoOutput.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); + when(mLeHearingDeviceInfoInput.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET); + when(mLeHearingDeviceInfoInput.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); + when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); when(mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).thenReturn( - new AudioDeviceInfo[]{mAudioDeviceInfo}); + new AudioDeviceInfo[]{mHearingDeviceInfoOutput}); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[]{mLeHearingDeviceInfoInput}); doReturn(Collections.emptyList()).when(mAudioManager).getPreferredDevicesForStrategy( any(AudioProductStrategy.class)); when(mAudioStrategy.getAudioAttributesForLegacyStreamType( AudioManager.STREAM_MUSIC)) .thenReturn((new AudioAttributes.Builder()).build()); - mHearingDeviceAttribute = new AudioDeviceAttributes( + mHearingDeviceAttributeOutput = new AudioDeviceAttributes( AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HEARING_AID, TEST_DEVICE_ADDRESS); @@ -99,11 +114,10 @@ public class HearingAidAudioRoutingHelperTest { @Test public void setPreferredDeviceRoutingStrategies_hadValueThenValueAuto_callRemoveStrategy() { when(mAudioManager.getPreferredDeviceForStrategy(mAudioStrategy)).thenReturn( - mHearingDeviceAttribute); + mHearingDeviceAttributeOutput); mHelper.setPreferredDeviceRoutingStrategies(List.of(mAudioStrategy), - mHearingDeviceAttribute, - HearingAidAudioRoutingConstants.RoutingValue.AUTO); + mHearingDeviceAttributeOutput, RoutingValue.AUTO); verify(mAudioManager, atLeastOnce()).removePreferredDeviceForStrategy(mAudioStrategy); } @@ -113,8 +127,7 @@ public class HearingAidAudioRoutingHelperTest { when(mAudioManager.getPreferredDeviceForStrategy(mAudioStrategy)).thenReturn(null); mHelper.setPreferredDeviceRoutingStrategies(List.of(mAudioStrategy), - mHearingDeviceAttribute, - HearingAidAudioRoutingConstants.RoutingValue.AUTO); + mHearingDeviceAttributeOutput, RoutingValue.AUTO); verify(mAudioManager, never()).removePreferredDeviceForStrategy(mAudioStrategy); } @@ -122,63 +135,95 @@ public class HearingAidAudioRoutingHelperTest { @Test public void setPreferredDeviceRoutingStrategies_valueHearingDevice_callSetStrategy() { mHelper.setPreferredDeviceRoutingStrategies(List.of(mAudioStrategy), - mHearingDeviceAttribute, - HearingAidAudioRoutingConstants.RoutingValue.HEARING_DEVICE); + mHearingDeviceAttributeOutput, RoutingValue.HEARING_DEVICE); verify(mAudioManager, atLeastOnce()).setPreferredDeviceForStrategy(mAudioStrategy, - mHearingDeviceAttribute); + mHearingDeviceAttributeOutput); } @Test - public void setPreferredDeviceRoutingStrategies_valueDeviceSpeaker_callSetStrategy() { - final AudioDeviceAttributes speakerDevice = new AudioDeviceAttributes( - AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, ""); + public void setPreferredDeviceRoutingStrategies_valueBuiltinDevice_callSetStrategy() { mHelper.setPreferredDeviceRoutingStrategies(List.of(mAudioStrategy), - mHearingDeviceAttribute, - HearingAidAudioRoutingConstants.RoutingValue.DEVICE_SPEAKER); + mHearingDeviceAttributeOutput, RoutingValue.BUILTIN_DEVICE); verify(mAudioManager, atLeastOnce()).setPreferredDeviceForStrategy(mAudioStrategy, - speakerDevice); + BUILTIN_SPEAKER); } @Test - public void getMatchedHearingDeviceAttributes_mainHearingDevice_equalAddress() { + public void getMatchedHearingDeviceAttributesForOutput_mainHearingDevice_equalAddress() { when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); - final String targetAddress = mHelper.getMatchedHearingDeviceAttributes( + final String targetAddress = mHelper.getMatchedHearingDeviceAttributesForOutput( mCachedBluetoothDevice).getAddress(); - assertThat(targetAddress).isEqualTo(mHearingDeviceAttribute.getAddress()); + assertThat(targetAddress).isEqualTo(mHearingDeviceAttributeOutput.getAddress()); } @Test - public void getMatchedHearingDeviceAttributes_subHearingDevice_equalAddress() { + public void getMatchedHearingDeviceAttributesForOutput_subHearingDevice_equalAddress() { when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); when(mCachedBluetoothDevice.getAddress()).thenReturn(NOT_EXPECT_DEVICE_ADDRESS); when(mCachedBluetoothDevice.getSubDevice()).thenReturn(mSubCachedBluetoothDevice); when(mSubCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); when(mSubCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); - final String targetAddress = mHelper.getMatchedHearingDeviceAttributes( + final String targetAddress = mHelper.getMatchedHearingDeviceAttributesForOutput( mCachedBluetoothDevice).getAddress(); - assertThat(targetAddress).isEqualTo(mHearingDeviceAttribute.getAddress()); + assertThat(targetAddress).isEqualTo(mHearingDeviceAttributeOutput.getAddress()); } @Test - public void getMatchedHearingDeviceAttributes_memberHearingDevice_equalAddress() { + public void getMatchedHearingDeviceAttributesForOutput_memberHearingDevice_equalAddress() { when(mSubCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); when(mSubCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); - final Set<CachedBluetoothDevice> memberDevices = new HashSet<CachedBluetoothDevice>(); + final Set<CachedBluetoothDevice> memberDevices = new HashSet<>(); memberDevices.add(mSubCachedBluetoothDevice); when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); when(mCachedBluetoothDevice.getAddress()).thenReturn(NOT_EXPECT_DEVICE_ADDRESS); when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(memberDevices); - final String targetAddress = mHelper.getMatchedHearingDeviceAttributes( + final String targetAddress = mHelper.getMatchedHearingDeviceAttributesForOutput( mCachedBluetoothDevice).getAddress(); - assertThat(targetAddress).isEqualTo(mHearingDeviceAttribute.getAddress()); + assertThat(targetAddress).isEqualTo(mHearingDeviceAttributeOutput.getAddress()); + } + + @Test + public void setPreferredInputDeviceForCalls_valueAuto_callClearPreset() { + when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); + + mHelper.setPreferredInputDeviceForCalls(mCachedBluetoothDevice, RoutingValue.AUTO); + + verify(mAudioManager).clearPreferredDevicesForCapturePreset( + MICROPHONE_SOURCE_VOICE_COMMUNICATION); + } + + @Test + public void setPreferredInputDeviceForCalls_valueHearingDevice_callSetPresetToHearingDevice() { + final ArgumentCaptor<AudioDeviceAttributes> audioDeviceAttributesCaptor = + ArgumentCaptor.forClass(AudioDeviceAttributes.class); + when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); + + mHelper.setPreferredInputDeviceForCalls(mCachedBluetoothDevice, + RoutingValue.HEARING_DEVICE); + + verify(mAudioManager).setPreferredDeviceForCapturePreset( + eq(MICROPHONE_SOURCE_VOICE_COMMUNICATION), audioDeviceAttributesCaptor.capture()); + assertThat(audioDeviceAttributesCaptor.getValue().getAddress()).isEqualTo( + TEST_DEVICE_ADDRESS); + } + + @Test + public void setPreferredInputDeviceForCalls_valueBuiltinDevice_callClearPresetToBuiltinMic() { + when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); + + mHelper.setPreferredInputDeviceForCalls(mCachedBluetoothDevice, + RoutingValue.BUILTIN_DEVICE); + + verify(mAudioManager).setPreferredDeviceForCapturePreset( + MICROPHONE_SOURCE_VOICE_COMMUNICATION, BUILTIN_MIC); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java index eb73eee90f0d..2458c5b2eb6e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java @@ -729,7 +729,7 @@ public class HearingAidDeviceManagerTest { @Test public void onActiveDeviceChanged_connected_callSetStrategies() { - when(mHelper.getMatchedHearingDeviceAttributes(mCachedDevice1)).thenReturn( + when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn( mHearingDeviceAttribute); when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), @@ -743,7 +743,7 @@ public class HearingAidDeviceManagerTest { @Test public void onActiveDeviceChanged_disconnected_callSetStrategiesWithAutoValue() { - when(mHelper.getMatchedHearingDeviceAttributes(mCachedDevice1)).thenReturn( + when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn( mHearingDeviceAttribute); when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false); doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(), diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 18bebd40b03a..b9f8c714175e 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -289,5 +289,6 @@ public class SecureSettings { Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, Settings.Secure.ADVANCED_PROTECTION_MODE, Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS, + Settings.Secure.EM_VALUE, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 1d7608d7d4d0..7c5e577b1d93 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -445,6 +445,8 @@ public class SecureSettingsValidators { Secure.RESOLUTION_MODE_UNKNOWN, Secure.RESOLUTION_MODE_FULL)); VALIDATORS.put(Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_SATURATION_LEVEL, new InclusiveIntegerRangeValidator(0, 10)); + VALIDATORS.put(Secure.EM_VALUE, + new InclusiveIntegerRangeValidator(0, 1)); VALIDATORS.put(Secure.CHARGE_OPTIMIZATION_MODE, new InclusiveIntegerRangeValidator(0, 10)); VALIDATORS.put(Secure.ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS, ANY_LONG_VALIDATOR); VALIDATORS.put(Secure.ON_DEVICE_INTELLIGENCE_UNBIND_TIMEOUT_MS, ANY_LONG_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 661a09553914..dedd7ebd1ef7 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2186,6 +2186,10 @@ class SettingsProtoDumpUtil { SecureSettingsProto.EvenDimmer.EVEN_DIMMER_MIN_NITS); p.end(evenDimmerToken); + dumpSetting(s, p, + Settings.Secure.EM_VALUE, + SecureSettingsProto.Accessibility.EM_VALUE); + final long gestureToken = p.start(SecureSettingsProto.GESTURE); dumpSetting(s, p, Settings.Secure.AWARE_ENABLED, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 9ab281217fbd..9c53afecad11 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.BlendMode @@ -43,6 +44,7 @@ import com.android.compose.animation.scene.SceneTransitionLayout import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.observableTransitionState import com.android.compose.animation.scene.transitions +import com.android.compose.modifiers.thenIf import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys @@ -158,6 +160,7 @@ fun CommunalContainer( transitions = sceneTransitions, ) } + val isUiBlurred by viewModel.isUiBlurred.collectAsStateWithLifecycle() val detector = remember { CommunalSwipeDetector() } @@ -174,9 +177,11 @@ fun CommunalContainer( onDispose { viewModel.setTransitionState(null) } } + val blurRadius = with(LocalDensity.current) { viewModel.blurRadiusPx.toDp() } + SceneTransitionLayout( state = state, - modifier = modifier.fillMaxSize(), + modifier = modifier.fillMaxSize().thenIf(isUiBlurred) { Modifier.blur(blurRadius) }, swipeSourceDetector = detector, swipeDetector = detector, ) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 3ff99a8d1758..46e0efa79ef5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -361,8 +361,9 @@ fun ContentScope.NotificationScrollingStack( val shadeScrollState by remember { derivedStateOf { ShadeScrollState( - // we are not scrolled to the top unless the scrim is at its maximum offset. - isScrolledToTop = scrimOffset.value >= 0f, + // we are not scrolled to the top unless the scroll position is zero, + // and the scrim is at its maximum offset + isScrolledToTop = scrimOffset.value >= 0f && scrollState.value == 0, scrollPosition = scrollState.value, maxScrollPosition = scrollState.maxValue, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index 3327fc020da3..f052e60246d2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -103,12 +103,15 @@ constructor( onScrimClicked = viewModel::onScrimClicked, ) { Column { - CollapsedShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = tintedIconManagerFactory::create, - createBatteryMeterViewController = batteryMeterViewControllerFactory::create, - statusBarIconController = statusBarIconController, - ) + if (viewModel.showHeader) { + CollapsedShadeHeader( + viewModelFactory = viewModel.shadeHeaderViewModelFactory, + createTintedIconManager = tintedIconManagerFactory::create, + createBatteryMeterViewController = + batteryMeterViewControllerFactory::create, + statusBarIconController = statusBarIconController, + ) + } ShadeBody(viewModel = viewModel.quickSettingsContainerViewModel) } @@ -178,8 +181,8 @@ fun ContentScope.QuickSettingsLayout( Column( verticalArrangement = Arrangement.spacedBy(QuickSettingsShade.Dimensions.Padding), horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .padding( + modifier = + modifier.padding( start = QuickSettingsShade.Dimensions.Padding, end = QuickSettingsShade.Dimensions.Padding, bottom = QuickSettingsShade.Dimensions.Padding, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index d70af2806430..b70f46c4b01c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -25,10 +25,12 @@ import android.provider.Settings import android.widget.RemoteViews import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.Flags.FLAG_COMMUNAL_RESPONSIVE_GRID import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_DIRECT_EDIT_MODE import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.communal.data.model.CommunalSmartspaceTimer import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository @@ -69,6 +71,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.transitions.blurConfig import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer @@ -184,6 +187,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { logcatLogBuffer("CommunalViewModelTest"), metricsLogger, kosmos.mediaCarouselController, + kosmos.blurConfig, ) } @@ -893,6 +897,20 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(selectedKey2).isEqualTo(key) } + @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + fun uiIsBlurred_whenPrimaryBouncerIsShowing() = + testScope.runTest { + val viewModel = createViewModel() + val isUiBlurred by collectLastValue(viewModel.isUiBlurred) + + kosmos.fakeKeyguardBouncerRepository.setPrimaryShow(true) + assertThat(isUiBlurred).isTrue() + + kosmos.fakeKeyguardBouncerRepository.setPrimaryShow(false) + assertThat(isUiBlurred).isFalse() + } + private suspend fun setIsMainUser(isMainUser: Boolean) { val user = if (isMainUser) MAIN_USER_INFO else SECONDARY_USER_INFO with(userRepository) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt index f8f6fe246563..466c9f96749f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt @@ -26,6 +26,7 @@ import androidx.media3.common.Player import androidx.media3.session.CommandButton import androidx.media3.session.MediaController as Media3Controller import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionResult import androidx.media3.session.SessionToken import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -41,6 +42,8 @@ import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.execution import com.google.common.collect.ImmutableList import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.ListenableFuture +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -60,6 +63,7 @@ private const val PACKAGE_NAME = "package_name" private const val CUSTOM_ACTION_NAME = "Custom Action" private const val CUSTOM_ACTION_COMMAND = "custom-action" +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWithLooper @RunWith(AndroidJUnit4::class) @@ -84,12 +88,14 @@ class Media3ActionFactoryTest : SysuiTestCase() { } } private val customLayout = ImmutableList.of<CommandButton>() + private val customCommandFuture = mock<ListenableFuture<SessionResult>>() private val media3Controller = mock<Media3Controller> { on { customLayout } doReturn customLayout on { sessionExtras } doReturn Bundle() on { isCommandAvailable(any()) } doReturn true on { isSessionCommandAvailable(any<SessionCommand>()) } doReturn true + on { sendCustomCommand(any(), any()) } doReturn customCommandFuture } private lateinit var underTest: Media3ActionFactory @@ -105,7 +111,7 @@ class Media3ActionFactoryTest : SysuiTestCase() { kosmos.mediaLogger, kosmos.looper, handler, - kosmos.testScope, + testScope, kosmos.execution, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt index 24f6b6d8566b..7ab8ab93c024 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.testKosmos @@ -124,6 +125,24 @@ class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() { assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade) } + @Test + fun showHeader_showsOnNarrowScreen() = + testScope.runTest { + kosmos.shadeRepository.setShadeLayoutWide(false) + runCurrent() + + assertThat(underTest.showHeader).isTrue() + } + + @Test + fun showHeader_hidesOnWideScreen() = + testScope.runTest { + kosmos.shadeRepository.setShadeLayoutWide(true) + runCurrent() + + assertThat(underTest.showHeader).isFalse() + } + private fun TestScope.lockDevice() { val currentScene by collectLastValue(sceneInteractor.currentScene) kosmos.powerInteractor.setAsleepForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt new file mode 100644 index 000000000000..8650e4b8cfce --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.media.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class MediaControlChipViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val underTest = kosmos.mediaControlChipViewModel + + @Test + fun chip_noActiveMedia_IsHidden() = + kosmos.runTest { + val chip by collectLastValue(underTest.chip) + + assertThat(chip).isInstanceOf(PopupChipModel.Hidden::class.java) + } + + @Test + fun chip_activeMedia_IsShown() = + kosmos.runTest { + val chip by collectLastValue(underTest.chip) + + val userMedia = MediaData(active = true, song = "test") + val instanceId = userMedia.instanceId + + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java) + } + + @Test + fun chip_songNameChanges_chipTextUpdated() = + kosmos.runTest { + val chip by collectLastValue(underTest.chip) + + val initialSongName = "Initial Song" + val newSongName = "New Song" + val userMedia = MediaData(active = true, song = initialSongName) + val instanceId = userMedia.instanceId + + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName) + + val updatedUserMedia = userMedia.copy(song = newSongName) + mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + + assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt index 74d7e19ea86c..fcbf0fe9a37a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt @@ -22,6 +22,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -35,12 +39,28 @@ import org.junit.runner.RunWith class StatusBarPopupChipsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope + private val mediaFilterRepository = kosmos.mediaFilterRepository private val underTest = kosmos.statusBarPopupChipsViewModel @Test - fun popupChips_allHidden_empty() = + fun shownPopupChips_allHidden_empty() = testScope.runTest { - val latest by collectLastValue(underTest.popupChips) - assertThat(latest).isEmpty() + val shownPopupChips by collectLastValue(underTest.shownPopupChips) + assertThat(shownPopupChips).isEmpty() + } + + @Test + fun shownPopupChips_activeMedia_restHidden_mediaControlChipShown() = + testScope.runTest { + val shownPopupChips by collectLastValue(underTest.shownPopupChips) + + val userMedia = MediaData(active = true, song = "test") + val instanceId = userMedia.instanceId + + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + assertThat(shownPopupChips).hasSize(1) + assertThat(shownPopupChips!!.first().chipId).isEqualTo(PopupChipId.MediaControl) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index 1a1af2eecc00..87abd0a4494a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.statusbar.notification.stack +import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import android.testing.TestableLooper.RunWithLooper import android.view.LayoutInflater @@ -20,6 +21,7 @@ import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState import com.android.systemui.util.mockito.mock import junit.framework.Assert.assertEquals @@ -30,6 +32,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.mock +import org.mockito.Mockito.spy import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -59,7 +62,7 @@ open class NotificationShelfTest : SysuiTestCase() { .inflate( /* resource = */ R.layout.status_bar_notification_shelf, /* root = */ root, - /* attachToRoot = */ false + /* attachToRoot = */ false, ) as NotificationShelf whenever(ambientState.largeScreenShadeInterpolator).thenReturn(largeScreenShadeInterpolator) @@ -128,6 +131,177 @@ open class NotificationShelfTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testAlignment_splitShade_LTR() { + // Given: LTR mode, split shade + val shelfSpy = + prepareShelfSpy(shelf, rtl = false, splitShade = true, width = 100, actualWidth = 40) + + // Then: shelf should align to end + assertTrue(shelfSpy.isAlignedToEnd) + assertTrue(shelfSpy.isAlignedToRight) + assertTrue(shelfSpy.mBackgroundNormal.alignToEnd) + assertTrue(shelfSpy.mShelfIcons.alignToEnd) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testAlignment_nonSplitShade_LTR() { + // Given: LTR mode, non split shade + val shelfSpy = + prepareShelfSpy(shelf, rtl = false, splitShade = false, width = 100, actualWidth = 40) + + // Then: shelf should not align to end + assertFalse(shelfSpy.isAlignedToEnd) + assertFalse(shelfSpy.isAlignedToRight) + assertFalse(shelfSpy.mBackgroundNormal.alignToEnd) + assertFalse(shelfSpy.mShelfIcons.alignToEnd) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testAlignment_splitShade_RTL() { + // Given: RTL mode, split shade + val shelfSpy = + prepareShelfSpy(shelf, rtl = true, splitShade = true, width = 100, actualWidth = 40) + + // Then: shelf should align to end, but to left due to RTL + assertTrue(shelfSpy.isAlignedToEnd) + assertFalse(shelfSpy.isAlignedToRight) + assertTrue(shelfSpy.mBackgroundNormal.alignToEnd) + assertTrue(shelfSpy.mShelfIcons.alignToEnd) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testAlignment_nonSplitShade_RTL() { + // Given: RTL mode, non split shade + val shelfSpy = + prepareShelfSpy(shelf, rtl = true, splitShade = false, width = 100, actualWidth = 40) + + // Then: shelf should not align to end, but to right due to RTL + assertFalse(shelfSpy.isAlignedToEnd) + assertTrue(shelfSpy.isAlignedToRight) + assertFalse(shelfSpy.mBackgroundNormal.alignToEnd) + assertFalse(shelfSpy.mShelfIcons.alignToEnd) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testGetShelfLeftBound_splitShade_LTR() { + // Given: LTR mode, split shade + val shelfSpy = + prepareShelfSpy(shelf, rtl = false, splitShade = true, width = 100, actualWidth = 40) + + // When: get the left bound of the shelf + val shelfLeftBound = shelfSpy.shelfLeftBound + + // Then: should be equal to shelf's width - actual width + assertEquals(60f, shelfLeftBound) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testGetShelfRightBound_splitShade_LTR() { + // Given: LTR mode, split shade, width 100, actual width 40 + val shelfSpy = + prepareShelfSpy(shelf, rtl = false, splitShade = true, width = 100, actualWidth = 40) + + // Then: the right bound of the shelf should be equal to shelf's width + assertEquals(100f, shelfSpy.shelfRightBound) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testGetShelfLeftBound_nonSplitShade_LTR() { + // Given: LTR mode, non split shade + val shelfSpy = + prepareShelfSpy(shelf, rtl = false, splitShade = false, width = 100, actualWidth = 40) + + // When: get the left bound of the shelf + val shelfLeftBound = shelfSpy.shelfLeftBound + + // Then: should be equal to 0f + assertEquals(0f, shelfLeftBound) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testGetShelfRightBound_nonSplitShade_LTR() { + // Given: LTR mode, non split shade, width 100, actual width 40 + val shelfSpy = + prepareShelfSpy(shelf, rtl = false, splitShade = false, width = 100, actualWidth = 40) + + // Then: the right bound of the shelf should be equal to shelf's actual width + assertEquals(40f, shelfSpy.shelfRightBound) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testGetShelfLeftBound_splitShade_RTL() { + // Given: RTL mode, split shade + val shelfSpy = + prepareShelfSpy(shelf, rtl = true, splitShade = true, width = 100, actualWidth = 40) + + // When: get the left bound of the shelf + val shelfLeftBound = shelfSpy.shelfLeftBound + + // Then: should be equal to 0f + assertEquals(0f, shelfLeftBound) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testGetShelfRightBound_splitShade_RTL() { + // Given: RTL mode, split shade, width 100, actual width 40 + val shelfSpy = + prepareShelfSpy(shelf, rtl = true, splitShade = true, width = 100, actualWidth = 40) + + // Then: the right bound of the shelf should be equal to shelf's actual width + assertEquals(40f, shelfSpy.shelfRightBound) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testGetShelfLeftBound_nonSplitShade_RTL() { + // Given: RTL mode, non split shade + val shelfSpy = + prepareShelfSpy(shelf, rtl = true, splitShade = false, width = 100, actualWidth = 40) + + // When: get the left bound of the shelf + val shelfLeftBound = shelfSpy.shelfLeftBound + + // Then: should be equal to shelf's width - actual width + assertEquals(60f, shelfLeftBound) + } + + @Test + @EnableFlags(NotificationMinimalism.FLAG_NAME) + fun testGetShelfRightBound_nonSplitShade_RTL() { + // Given: LTR mode, non split shade, width 100, actual width 40 + val shelfSpy = + prepareShelfSpy(shelf, rtl = true, splitShade = false, width = 100, actualWidth = 40) + + // Then: the right bound of the shelf should be equal to shelf's width + assertEquals(100f, shelfSpy.shelfRightBound) + } + + private fun prepareShelfSpy( + shelf: NotificationShelf, + rtl: Boolean, + splitShade: Boolean, + width: Int, + actualWidth: Int, + ): NotificationShelf { + val shelfSpy = spy(shelf) + whenever(shelfSpy.isLayoutRtl).thenReturn(rtl) + whenever(ambientState.useSplitShade).thenReturn(splitShade) + whenever(shelfSpy.width).thenReturn(width) + shelfSpy.setActualWidth(actualWidth.toFloat()) + return shelfSpy + } + + @Test fun getAmountInShelf_lastViewBelowShelf_completelyInShelf() { val shelfClipStart = 0f val viewStart = 1f @@ -152,7 +326,7 @@ open class NotificationShelfTest : SysuiTestCase() { /* scrollingFast= */ false, /* expandingAnimated= */ false, /* isLastChild= */ true, - shelfClipStart + shelfClipStart, ) assertEquals(1f, amountInShelf) } @@ -182,7 +356,7 @@ open class NotificationShelfTest : SysuiTestCase() { /* scrollingFast= */ false, /* expandingAnimated= */ false, /* isLastChild= */ true, - shelfClipStart + shelfClipStart, ) assertEquals(1f, amountInShelf) } @@ -212,7 +386,7 @@ open class NotificationShelfTest : SysuiTestCase() { /* scrollingFast= */ false, /* expandingAnimated= */ false, /* isLastChild= */ true, - shelfClipStart + shelfClipStart, ) assertEquals(0.5f, amountInShelf) } @@ -241,7 +415,7 @@ open class NotificationShelfTest : SysuiTestCase() { /* scrollingFast= */ false, /* expandingAnimated= */ false, /* isLastChild= */ true, - shelfClipStart + shelfClipStart, ) assertEquals(0f, amountInShelf) } @@ -250,7 +424,7 @@ open class NotificationShelfTest : SysuiTestCase() { fun updateState_expansionChanging_shelfTransparent() { updateState_expansionChanging_shelfAlphaUpdated( expansionFraction = 0.25f, - expectedAlpha = 0.0f + expectedAlpha = 0.0f, ) } @@ -260,7 +434,7 @@ open class NotificationShelfTest : SysuiTestCase() { updateState_expansionChanging_shelfAlphaUpdated( expansionFraction = 0.85f, - expectedAlpha = 0.0f + expectedAlpha = 0.0f, ) } @@ -281,7 +455,7 @@ open class NotificationShelfTest : SysuiTestCase() { updateState_expansionChanging_shelfAlphaUpdated( expansionFraction = expansionFraction, - expectedAlpha = 0.123f + expectedAlpha = 0.123f, ) } @@ -330,7 +504,7 @@ open class NotificationShelfTest : SysuiTestCase() { /* scrollingFast= */ false, /* expandingAnimated= */ false, /* isLastChild= */ true, - shelfClipStart + shelfClipStart, ) assertEquals(1f, amountInShelf) } @@ -628,7 +802,7 @@ open class NotificationShelfTest : SysuiTestCase() { private fun updateState_expansionChanging_shelfAlphaUpdated( expansionFraction: Float, - expectedAlpha: Float + expectedAlpha: Float, ) { val sbnMock: StatusBarNotification = mock() val mockEntry = mock<NotificationEntry>().apply { whenever(this.sbn).thenReturn(sbnMock) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt index 0aaf89a4c382..a9db0b70dd4d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt @@ -23,6 +23,7 @@ import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -41,6 +42,8 @@ class FakeHomeStatusBarViewModel( override val ongoingActivityChips = MutableStateFlow(MultipleOngoingActivityChipsModel()) + override val statusBarPopupChips = MutableStateFlow(emptyList<PopupChipModel.Shown>()) + override val isHomeStatusBarAllowedByScene = MutableStateFlow(false) override val shouldShowOperatorNameView = MutableStateFlow(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGesture.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGesture.kt new file mode 100644 index 000000000000..68b5772bd7ac --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGesture.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.touchpad.tutorial.ui.gesture + +import android.graphics.PointF +import android.view.MotionEvent +import kotlin.math.cos +import kotlin.math.sin + +/** Test helper to generate circular gestures or full EasterEgg gesture */ +object EasterEggGesture { + + fun motionEventsForGesture(): List<MotionEvent> { + val gesturePath = generateCircularGesturePoints(circlesCount = 3) + val events = + TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { p -> move(p.x, p.y) } } + return events + } + + /** + * Generates list of points that would make up clockwise circular motion with given [radius]. + * [circlesCount] determines how many full circles gesture should perform. [radiusNoiseFraction] + * can introduce noise to mimic real-world gesture which is not perfect - shape will be still + * circular but radius at any given point can be deviate from given radius by + * [radiusNoiseFraction]. + */ + fun generateCircularGesturePoints( + circlesCount: Int, + radiusNoiseFraction: Double? = null, + radius: Float = 100f, + ): List<PointF> { + val pointsPerCircle = 50 + val angleStep = 360 / pointsPerCircle + val angleBuffer = 20 // buffer to make sure we're doing a bit more than 360 degree + val totalAngle = circlesCount * (360 + angleBuffer) + // Because all gestures in tests should start at (DEFAULT_X, DEFAULT_Y) we need to shift + // circle center x coordinate by radius + val centerX = -radius + val centerY = 0f + + val randomNoise: (Double) -> Double = + if (radiusNoiseFraction == null) { + { 0.0 } + } else { + { radianAngle -> sin(radianAngle * 2) * radiusNoiseFraction } + } + + val events = mutableListOf<PointF>() + var currentAngle = 0f + // as cos(0) == 1 and sin(0) == 0 we start gesture at position of (radius, 0) and go + // clockwise - first Y increases and X decreases + while (currentAngle < totalAngle) { + val radianAngle = Math.toRadians(currentAngle.toDouble()) + val radiusWithNoise = radius * (1 + randomNoise(radianAngle).toFloat()) + val x = centerX + radiusWithNoise * cos(radianAngle).toFloat() + val y = centerY + radiusWithNoise * sin(radianAngle).toFloat() + events.add(PointF(x, y)) + currentAngle += angleStep + } + return events + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureRecognizerTest.kt index ff0cec5e06e9..3829778864f1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureRecognizerTest.kt @@ -16,29 +16,28 @@ package com.android.systemui.touchpad.tutorial.ui.gesture +import android.graphics.PointF import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE +import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGesture.generateCircularGesturePoints import com.google.common.truth.Truth.assertThat -import kotlin.math.cos -import kotlin.math.sin +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -class EasterEggGestureTest : SysuiTestCase() { - - private data class Point(val x: Float, val y: Float) +class EasterEggGestureRecognizerTest : SysuiTestCase() { private var triggered = false - private val handler = - TouchpadGestureHandler( - BackGestureRecognizer(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()), - EasterEggGestureMonitor(callback = { triggered = true }), - ) + private val gestureRecognizer = EasterEggGestureRecognizer() + + @Before + fun setup() { + gestureRecognizer.addGestureStateCallback { triggered = it == GestureState.Finished } + } @Test fun easterEggTriggeredAfterThreeCircles() { @@ -99,56 +98,13 @@ class EasterEggGestureTest : SysuiTestCase() { } private fun assertStateAfterEvents(events: List<MotionEvent>, wasTriggered: Boolean) { - events.forEach { handler.onMotionEvent(it) } + events.forEach { gestureRecognizer.accept(it) } assertThat(triggered).isEqualTo(wasTriggered) } - private fun assertStateAfterTwoFingerGesture(gesturePath: List<Point>, wasTriggered: Boolean) { + private fun assertStateAfterTwoFingerGesture(gesturePath: List<PointF>, wasTriggered: Boolean) { val events = - TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { (x, y) -> move(x, y) } } + TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { p -> move(p.x, p.y) } } assertStateAfterEvents(events = events, wasTriggered = wasTriggered) } - - /** - * Generates list of points that would make up clockwise circular motion with given [radius]. - * [circlesCount] determines how many full circles gesture should perform. [radiusNoiseFraction] - * can introduce noise to mimic real-world gesture which is not perfect - shape will be still - * circular but radius at any given point can be deviate from given radius by - * [radiusNoiseFraction]. - */ - private fun generateCircularGesturePoints( - circlesCount: Int, - radiusNoiseFraction: Double? = null, - radius: Float = 100f, - ): List<Point> { - val pointsPerCircle = 50 - val angleStep = 360 / pointsPerCircle - val angleBuffer = 20 // buffer to make sure we're doing a bit more than 360 degree - val totalAngle = circlesCount * (360 + angleBuffer) - // Because all gestures in tests should start at (DEFAULT_X, DEFAULT_Y) we need to shift - // circle center x coordinate by radius - val centerX = -radius - val centerY = 0f - - val events = mutableListOf<Point>() - val randomNoise: (Double) -> Double = - if (radiusNoiseFraction == null) { - { 0.0 } - } else { - { radianAngle -> sin(radianAngle * 2) * radiusNoiseFraction } - } - - var currentAngle = 0f - // as cos(0) == 1 and sin(0) == 0 we start gesture at position of (radius, 0) and go - // clockwise - first Y increases and X decreases - while (currentAngle < totalAngle) { - val radianAngle = Math.toRadians(currentAngle.toDouble()) - val radiusWithNoise = radius * (1 + randomNoise(radianAngle).toFloat()) - val x = centerX + radiusWithNoise * cos(radianAngle).toFloat() - val y = centerY + radiusWithNoise * sin(radianAngle).toFloat() - events.add(Point(x, y)) - currentAngle += angleStep - } - return events - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadEventsFilterTest.kt index c302b40fc4d7..20bcb3eac8a0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadEventsFilterTest.kt @@ -33,12 +33,11 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -class TouchpadGestureHandlerTest : SysuiTestCase() { +class TouchpadEventsFilterTest : SysuiTestCase() { private var gestureState: GestureState = GestureState.NotStarted private val gestureRecognizer = BackGestureRecognizer(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()) - private val handler = TouchpadGestureHandler(gestureRecognizer, EasterEggGestureMonitor {}) @Before fun before() { @@ -48,21 +47,21 @@ class TouchpadGestureHandlerTest : SysuiTestCase() { @Test fun handlesEventsFromTouchpad() { val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_FINGER) - val eventHandled = handler.onMotionEvent(event) + val eventHandled = gestureRecognizer.handleTouchpadMotionEvent(event) assertThat(eventHandled).isTrue() } @Test fun ignoresEventsFromMouse() { val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_MOUSE) - val eventHandled = handler.onMotionEvent(event) + val eventHandled = gestureRecognizer.handleTouchpadMotionEvent(event) assertThat(eventHandled).isFalse() } @Test fun ignoresEventsFromTouch() { val event = downEvent(source = SOURCE_TOUCHSCREEN, toolType = TOOL_TYPE_FINGER) - val eventHandled = handler.onMotionEvent(event) + val eventHandled = gestureRecognizer.handleTouchpadMotionEvent(event) assertThat(eventHandled).isFalse() } @@ -70,25 +69,10 @@ class TouchpadGestureHandlerTest : SysuiTestCase() { fun ignoresButtonClicksFromTouchpad() { val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_FINGER) event.buttonState = MotionEvent.BUTTON_PRIMARY - val eventHandled = handler.onMotionEvent(event) + val eventHandled = gestureRecognizer.handleTouchpadMotionEvent(event) assertThat(eventHandled).isFalse() } private fun downEvent(source: Int, toolType: Int) = motionEvent(action = ACTION_DOWN, x = 0f, y = 0f, source = source, toolType = toolType) - - @Test - fun triggersGestureDoneForThreeFingerGesture() { - backGestureEvents().forEach { handler.onMotionEvent(it) } - - assertThat(gestureState).isEqualTo(GestureState.Finished) - } - - private fun backGestureEvents(): List<MotionEvent> { - return ThreeFingerGesture.eventsForFullGesture { - move(deltaX = SWIPE_DISTANCE / 4) - move(deltaX = SWIPE_DISTANCE / 2) - move(deltaX = SWIPE_DISTANCE) - } - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt index f90e14caca75..79c1f9fcf517 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt @@ -54,13 +54,6 @@ class BackGestureScreenViewModelTest : SysuiTestCase() { } @Test - fun easterEggNotTriggeredAtStart() = - kosmos.runTest { - val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) - assertThat(easterEggTriggered).isFalse() - } - - @Test fun emitsProgressStateWithLeftProgressAnimation() = kosmos.runTest { assertProgressWhileMovingFingers( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModelTest.kt new file mode 100644 index 000000000000..4af374287c62 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModelTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.touchpad.tutorial.ui.viewmodel + +import android.view.MotionEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGesture +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class EasterEggGestureViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val viewModel = EasterEggGestureViewModel() + + @Before + fun before() { + kosmos.useUnconfinedTestDispatcher() + } + + @Test + fun easterEggNotTriggeredAtStart() = + kosmos.runTest { + val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) + assertThat(easterEggTriggered).isFalse() + } + + @Test + fun emitsTrueOnEasterEggTriggered() = + kosmos.runTest { + assertStateAfterEvents( + events = EasterEggGesture.motionEventsForGesture(), + expected = true, + ) + } + + @Test + fun emitsFalseOnEasterEggCallbackExecuted() = + kosmos.runTest { + val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) + EasterEggGesture.motionEventsForGesture().forEach { viewModel.accept(it) } + + assertThat(easterEggTriggered).isEqualTo(true) + viewModel.onEasterEggFinished() + assertThat(easterEggTriggered).isEqualTo(false) + } + + private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: Boolean) { + val state by collectLastValue(viewModel.easterEggTriggered) + events.forEach { viewModel.accept(it) } + assertThat(state).isEqualTo(expected) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt index 3c06352ace97..4dfd01a91f17 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt @@ -70,13 +70,6 @@ class HomeGestureScreenViewModelTest : SysuiTestCase() { } @Test - fun easterEggNotTriggeredAtStart() = - kosmos.runTest { - val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) - assertThat(easterEggTriggered).isFalse() - } - - @Test fun emitsProgressStateWithAnimationMarkers() = kosmos.runTest { assertStateAfterEvents( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt index a2d8a8b3cb0e..66bf778a754b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt @@ -74,13 +74,6 @@ class RecentAppsGestureScreenViewModelTest : SysuiTestCase() { } @Test - fun easterEggNotTriggeredAtStart() = - kosmos.runTest { - val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) - assertThat(easterEggTriggered).isFalse() - } - - @Test fun emitsProgressStateWithAnimationMarkers() = kosmos.runTest { assertStateAfterEvents( diff --git a/packages/SystemUI/res/layout/status_bar_notification_shelf.xml b/packages/SystemUI/res/layout/status_bar_notification_shelf.xml index 58c545036b27..071b07631ff9 100644 --- a/packages/SystemUI/res/layout/status_bar_notification_shelf.xml +++ b/packages/SystemUI/res/layout/status_bar_notification_shelf.xml @@ -24,11 +24,11 @@ android:clickable="true" > - <com.android.systemui.statusbar.notification.row.NotificationBackgroundView + <com.android.systemui.statusbar.notification.shelf.NotificationShelfBackgroundView android:id="@+id/backgroundNormal" android:layout_width="match_parent" android:layout_height="match_parent" /> - <com.android.systemui.statusbar.phone.NotificationIconContainer + <com.android.systemui.statusbar.notification.shelf.NotificationShelfIconContainer android:id="@+id/content" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 0503dbfab71d..94698bcb88b8 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -570,6 +570,8 @@ <item name="trackCornerSize">12dp</item> <item name="trackInsideCornerSize">2dp</item> <item name="trackStopIndicatorSize">6dp</item> + <item name="trackIconSize">20dp</item> + <item name="labelBehavior">gone</item> </style> <style name="SystemUI.Material3.Slider" parent="@style/Widget.Material3.Slider"> @@ -579,6 +581,7 @@ <item name="tickColorInactive">@androidprv:color/materialColorPrimary</item> <item name="trackColorActive">@androidprv:color/materialColorPrimary</item> <item name="trackColorInactive">@androidprv:color/materialColorSurfaceContainerHighest</item> + <item name="trackIconActiveColor">@androidprv:color/materialColorSurfaceContainerHighest</item> </style> <style name="Theme.SystemUI.DayNightDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"/> diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index ddc4d1c10690..16cf26393aee 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -33,6 +33,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.ui.transitions.BlurConfig import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog @@ -92,6 +93,7 @@ constructor( @CommunalLog logBuffer: LogBuffer, private val metricsLogger: CommunalMetricsLogger, mediaCarouselController: MediaCarouselController, + blurConfig: BlurConfig, ) : BaseCommunalViewModel( communalSceneInteractor, @@ -221,6 +223,15 @@ constructor( val isEnableWorkProfileDialogShowing: Flow<Boolean> = _isEnableWorkProfileDialogShowing.asStateFlow() + val isUiBlurred: StateFlow<Boolean> = + if (Flags.bouncerUiRevamp()) { + keyguardInteractor.primaryBouncerShowing + } else { + MutableStateFlow(false) + } + + val blurRadiusPx: Float = blurConfig.maxBlurRadiusPx / 2.0f + init { // Initialize our media host for the UMO. This only needs to happen once and must be done // before the MediaHierarchyManager attempts to move the UMO to the hub. diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt index 913aa6f9d547..09544827a51a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt @@ -43,6 +43,7 @@ import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.media.controls.util.SessionTokenFactory import com.android.systemui.res.R import com.android.systemui.util.concurrency.Execution +import java.util.concurrent.ExecutionException import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -71,7 +72,7 @@ constructor( * * @param packageName Package name for the media app * @param controller The framework [MediaController] for the session - * @return The media action buttons, or null if the session token is null + * @return The media action buttons, or null if cannot be created for this session */ suspend fun createActionsFromSession( packageName: String, @@ -80,6 +81,10 @@ constructor( // Get the Media3 controller using the legacy token val token = tokenFactory.createTokenFromLegacy(sessionToken) val m3controller = controllerFactory.create(token, looper) + if (m3controller == null) { + logger.logCreateFailed(packageName, "createActionsFromSession") + return null + } // Build button info val buttons = suspendCancellableCoroutine { continuation -> @@ -89,13 +94,14 @@ constructor( val result = getMedia3Actions(packageName, m3controller, token) continuation.resumeWith(Result.success(result)) } finally { - m3controller.release() + m3controller.tryRelease(packageName, logger) } } handler.post(runnable) continuation.invokeOnCancellation { // Ensure controller is released, even if loading was cancelled partway through - handler.post(m3controller::release) + val releaseRunnable = Runnable { m3controller.tryRelease(packageName, logger) } + handler.post(releaseRunnable) handler.removeCallbacks(runnable) } } @@ -127,11 +133,12 @@ constructor( com.android.internal.R.drawable.progress_small_material, ) } else { - getStandardAction(m3controller, token, Player.COMMAND_PLAY_PAUSE) + getStandardAction(packageName, m3controller, token, Player.COMMAND_PLAY_PAUSE) } val prevButton = getStandardAction( + packageName, m3controller, token, Player.COMMAND_SEEK_TO_PREVIOUS, @@ -139,6 +146,7 @@ constructor( ) val nextButton = getStandardAction( + packageName, m3controller, token, Player.COMMAND_SEEK_TO_NEXT, @@ -208,6 +216,7 @@ constructor( * @return A [MediaAction] representing the first supported command, or null if not supported */ private fun getStandardAction( + packageName: String, controller: Media3Controller, token: SessionToken, vararg commands: @Player.Command Int, @@ -222,14 +231,14 @@ constructor( if (!controller.isPlaying) { MediaAction( context.getDrawable(R.drawable.ic_media_play), - { executeAction(token, Player.COMMAND_PLAY_PAUSE) }, + { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) }, context.getString(R.string.controls_media_button_play), context.getDrawable(R.drawable.ic_media_play_container), ) } else { MediaAction( context.getDrawable(R.drawable.ic_media_pause), - { executeAction(token, Player.COMMAND_PLAY_PAUSE) }, + { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) }, context.getString(R.string.controls_media_button_pause), context.getDrawable(R.drawable.ic_media_pause_container), ) @@ -238,7 +247,7 @@ constructor( else -> { MediaAction( icon = getIconForAction(command), - action = { executeAction(token, command) }, + action = { executeAction(packageName, token, command) }, contentDescription = getDescriptionForAction(command), background = null, ) @@ -256,7 +265,7 @@ constructor( ): MediaAction { return MediaAction( getIconForAction(customAction, packageName), - { executeAction(token, Player.COMMAND_INVALID, customAction) }, + { executeAction(packageName, token, Player.COMMAND_INVALID, customAction) }, customAction.displayName, null, ) @@ -308,12 +317,17 @@ constructor( } private fun executeAction( + packageName: String, token: SessionToken, command: Int, customAction: CommandButton? = null, ) { bgScope.launch { val controller = controllerFactory.create(token, looper) + if (controller == null) { + logger.logCreateFailed(packageName, "executeAction") + return@launch + } handler.post { try { when (command) { @@ -347,9 +361,17 @@ constructor( else -> logger.logMedia3UnsupportedCommand(command.toString()) } } finally { - controller.release() + controller.tryRelease(packageName, logger) } } } } } + +private fun Media3Controller.tryRelease(packageName: String, logger: MediaLogger) { + try { + this.release() + } catch (e: ExecutionException) { + logger.logReleaseFailed(packageName, e.cause.toString()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt index 0b598c13311f..c52268e35e0e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt @@ -144,6 +144,30 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { buffer.log(TAG, LogLevel.DEBUG, { str1 = command }, { "Unsupported media3 command $str1" }) } + fun logCreateFailed(pkg: String, method: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = pkg + str2 = method + }, + { "Controller create failed for $str1 ($str2)" }, + ) + } + + fun logReleaseFailed(pkg: String, cause: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = pkg + str2 = cause + }, + { "Controller release failed for $str1 ($str2)" }, + ) + } + companion object { private const val TAG = "MediaLog" } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.kt index d815852b790f..7b9e18a0744b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.kt @@ -19,13 +19,17 @@ import android.content.Context import android.media.session.MediaController import android.media.session.MediaSession import android.os.Looper +import android.util.Log import androidx.concurrent.futures.await import androidx.media3.session.MediaController as Media3Controller import androidx.media3.session.SessionToken +import java.util.concurrent.ExecutionException import javax.inject.Inject /** Testable wrapper for media controller construction */ open class MediaControllerFactory @Inject constructor(private val context: Context) { + private val TAG = "MediaControllerFactory" + /** * Creates a new [MediaController] from the framework session token. * @@ -41,10 +45,18 @@ open class MediaControllerFactory @Inject constructor(private val context: Conte * @param token The token for the session * @param looper The looper that will be used for this controller's operations */ - open suspend fun create(token: SessionToken, looper: Looper): Media3Controller { - return Media3Controller.Builder(context, token) - .setApplicationLooper(looper) - .buildAsync() - .await() + open suspend fun create(token: SessionToken, looper: Looper): Media3Controller? { + try { + return Media3Controller.Builder(context, token) + .setApplicationLooper(looper) + .buildAsync() + .await() + } catch (e: ExecutionException) { + if (e.cause is SecurityException) { + // The session rejected the connection + Log.d(TAG, "SecurityException creating media3 controller") + } + return null + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt index 8ef51af18881..c0c0aea073f1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt @@ -16,8 +16,10 @@ package com.android.systemui.qs.ui.viewmodel +import androidx.compose.runtime.getValue import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -28,6 +30,8 @@ import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch /** * Models UI state used to render the content of the quick settings shade overlay. @@ -44,10 +48,21 @@ constructor( quickSettingsContainerViewModelFactory: QuickSettingsContainerViewModel.Factory, ) : ExclusiveActivatable() { + private val hydrator = Hydrator("QuickSettingsContainerViewModel.hydrator") + + val showHeader: Boolean by + hydrator.hydratedStateOf( + traceName = "showHeader", + initialValue = !shadeInteractor.isShadeLayoutWide.value, + source = shadeInteractor.isShadeLayoutWide.map { !it }, + ) + val quickSettingsContainerViewModel = quickSettingsContainerViewModelFactory.create(false) override suspend fun onActivated(): Nothing { coroutineScope { + launch { hydrator.activate() } + launch { sceneInteractor.currentScene.collect { currentScene -> when (currentScene) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index d523bc1867c2..48cf7a83c324 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -48,6 +48,9 @@ import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism; +import com.android.systemui.statusbar.notification.shelf.NotificationShelfBackgroundView; +import com.android.systemui.statusbar.notification.shelf.NotificationShelfIconContainer; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; @@ -76,7 +79,11 @@ public class NotificationShelf extends ActivatableNotificationView { private static final SourceType BASE_VALUE = SourceType.from("BaseValue"); private static final SourceType SHELF_SCROLL = SourceType.from("ShelfScroll"); - private NotificationIconContainer mShelfIcons; + @VisibleForTesting + public NotificationShelfIconContainer mShelfIcons; + // This field hides mBackgroundNormal from super class for short-shelf alignment + @VisibleForTesting + public NotificationShelfBackgroundView mBackgroundNormal; private boolean mHideBackground; private int mStatusBarHeight; private boolean mEnableNotificationClipping; @@ -116,6 +123,8 @@ public class NotificationShelf extends ActivatableNotificationView { mShelfIcons.setClipChildren(false); mShelfIcons.setClipToPadding(false); + mBackgroundNormal = (NotificationShelfBackgroundView) super.mBackgroundNormal; + setClipToActualHeight(false); setClipChildren(false); setClipToPadding(false); @@ -268,19 +277,37 @@ public class NotificationShelf extends ActivatableNotificationView { } } - private void setActualWidth(float actualWidth) { + /** + * Set the actual width of the shelf, this will only differ from width for short shelves. + */ + @VisibleForTesting + public void setActualWidth(float actualWidth) { setBackgroundWidth((int) actualWidth); if (mShelfIcons != null) { + mShelfIcons.setAlignToEnd(isAlignedToEnd()); mShelfIcons.setActualLayoutWidth((int) actualWidth); } mActualWidth = actualWidth; } @Override + public void setBackgroundWidth(int width) { + super.setBackgroundWidth(width); + if (!NotificationMinimalism.isEnabled()) { + return; + } + if (mBackgroundNormal != null) { + mBackgroundNormal.setAlignToEnd(isAlignedToEnd()); + } + } + + @Override public void getBoundsOnScreen(Rect outRect, boolean clipToParent) { super.getBoundsOnScreen(outRect, clipToParent); final int actualWidth = getActualWidth(); - if (isLayoutRtl()) { + final boolean alignedToRight = NotificationMinimalism.isEnabled() ? isAlignedToRight() : + isLayoutRtl(); + if (alignedToRight) { outRect.left = outRect.right - actualWidth; } else { outRect.right = outRect.left + actualWidth; @@ -326,11 +353,17 @@ public class NotificationShelf extends ActivatableNotificationView { */ @Override public boolean pointInView(float localX, float localY, float slop) { - final float containerWidth = getWidth(); - final float shelfWidth = getActualWidth(); + final float left, right; - final float left = isLayoutRtl() ? containerWidth - shelfWidth : 0; - final float right = isLayoutRtl() ? containerWidth : shelfWidth; + if (NotificationMinimalism.isEnabled()) { + left = getShelfLeftBound(); + right = getShelfRightBound(); + } else { + final float containerWidth = getWidth(); + final float shelfWidth = getActualWidth(); + left = isLayoutRtl() ? containerWidth - shelfWidth : 0; + right = isLayoutRtl() ? containerWidth : shelfWidth; + } final float top = mClipTopAmount; final float bottom = getActualHeight(); @@ -339,10 +372,53 @@ public class NotificationShelf extends ActivatableNotificationView { && isYInView(localY, slop, top, bottom); } + /** + * @return The left boundary of the shelf. + */ + @VisibleForTesting + public float getShelfLeftBound() { + if (isAlignedToRight()) { + return getWidth() - getActualWidth(); + } else { + return 0; + } + } + + /** + * @return The right boundary of the shelf. + */ + @VisibleForTesting + public float getShelfRightBound() { + if (isAlignedToRight()) { + return getWidth(); + } else { + return getActualWidth(); + } + } + + @VisibleForTesting + public boolean isAlignedToRight() { + return isAlignedToEnd() ^ isLayoutRtl(); + } + + /** + * When notification minimalism is on, on split shade, we want the notification shelf to align + * to the layout end (right for LTR; left for RTL). + * @return whether to align with the minimalism split shade style + */ + @VisibleForTesting + public boolean isAlignedToEnd() { + if (!NotificationMinimalism.isEnabled()) { + return false; + } + return mAmbientState.getUseSplitShade(); + } + @Override public void updateBackgroundColors() { super.updateBackgroundColors(); ColorUpdateLogger colorUpdateLogger = ColorUpdateLogger.getInstance(); + if (colorUpdateLogger != null) { colorUpdateLogger.logEvent("Shelf.updateBackgroundColors()", "normalBgColor=" + hexColorString(getNormalBgColor()) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt index 85c67f5b55a2..4e68bee295fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt @@ -41,14 +41,14 @@ import kotlinx.coroutines.flow.stateIn class MediaControlChipInteractor @Inject constructor( - @Background private val applicationScope: CoroutineScope, + @Background private val backgroundScope: CoroutineScope, mediaFilterRepository: MediaFilterRepository, ) { private val currentMediaControls: StateFlow<List<MediaCommonModel.MediaControl>> = mediaFilterRepository.currentMedia .map { mediaList -> mediaList.filterIsInstance<MediaCommonModel.MediaControl>() } .stateIn( - scope = applicationScope, + scope = backgroundScope, started = SharingStarted.WhileSubscribed(), initialValue = emptyList(), ) @@ -64,7 +64,7 @@ constructor( ?.toMediaControlChipModel() } .stateIn( - scope = applicationScope, + scope = backgroundScope, started = SharingStarted.WhileSubscribed(), initialValue = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt new file mode 100644 index 000000000000..3e854b4dbaf8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.media.ui.viewmodel + +import android.content.Context +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.dagger.qualifiers.Background +import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor +import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel +import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * [StatusBarPopupChipViewModel] for a media control chip in the status bar. This view model is + * responsible for converting the [MediaControlChipModel] to a [PopupChipModel] that can be used to + * display a media control chip. + */ +@SysUISingleton +class MediaControlChipViewModel +@Inject +constructor( + @Background private val backgroundScope: CoroutineScope, + @Application private val applicationContext: Context, + mediaControlChipInteractor: MediaControlChipInteractor, +) : StatusBarPopupChipViewModel { + + /** + * A [StateFlow] of the current [PopupChipModel]. This flow emits a new [PopupChipModel] + * whenever the underlying [MediaControlChipModel] changes. + */ + override val chip: StateFlow<PopupChipModel> = + mediaControlChipInteractor.mediaControlModel + .map { mediaControlModel -> toPopupChipModel(mediaControlModel, applicationContext) } + .stateIn( + backgroundScope, + SharingStarted.WhileSubscribed(), + PopupChipModel.Hidden(PopupChipId.MediaControl), + ) +} + +private fun toPopupChipModel(model: MediaControlChipModel?, context: Context): PopupChipModel { + if (model == null || model.songName.isNullOrEmpty()) { + return PopupChipModel.Hidden(PopupChipId.MediaControl) + } + + val contentDescription = model.appName?.let { ContentDescription.Loaded(description = it) } + return PopupChipModel.Shown( + chipId = PopupChipId.MediaControl, + icon = + model.appIcon?.loadDrawable(context)?.let { + Icon.Loaded(drawable = it, contentDescription = contentDescription) + } + ?: Icon.Resource( + res = com.android.internal.R.drawable.ic_audio_media, + contentDescription = contentDescription, + ), + chipText = model.songName.toString(), + // TODO(b/385202114): Show a popup containing the media carousal when the chip is toggled. + onToggle = {}, + // TODO(b/385202193): Add support for clicking on the icon on a media chip. + onIconPressed = {}, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt index 1663aebd7287..0a6c4d0fd14f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt @@ -23,7 +23,7 @@ import com.android.systemui.common.shared.model.Icon * displaying its popup at a time. */ sealed class PopupChipId(val value: String) { - data object MediaControls : PopupChipId("MediaControls") + data object MediaControl : PopupChipId("MediaControl") } /** Model for individual status bar popup chips. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChipsContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChipsContainer.kt new file mode 100644 index 000000000000..56bbd74af1c2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChipsContainer.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.popups.ui.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel + +/** Container view that holds all right hand side chips in the status bar. */ +@Composable +fun StatusBarPopupChipsContainer(chips: List<PopupChipModel.Shown>, modifier: Modifier = Modifier) { + // TODO(b/385353140): Add padding and spacing for this container according to UX specs. + Box { + Row(verticalAlignment = Alignment.CenterVertically) { + // TODO(b/385352859): Show `StatusBarPopupChip` here instead of `Text` once it is ready. + chips.forEach { chip -> Text(text = chip.chipText) } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt index b390f29b166c..caa8e6cc02c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt @@ -18,13 +18,16 @@ package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.MediaControlChipViewModel +import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -33,16 +36,29 @@ import kotlinx.coroutines.flow.stateIn * PopupChipModels. */ @SysUISingleton -class StatusBarPopupChipsViewModel @Inject constructor(@Background scope: CoroutineScope) { +class StatusBarPopupChipsViewModel +@Inject +constructor( + @Background scope: CoroutineScope, + mediaControlChipViewModel: MediaControlChipViewModel, +) { private data class PopupChipBundle( - val media: PopupChipModel = PopupChipModel.Hidden(chipId = PopupChipId.MediaControls) + val media: PopupChipModel = PopupChipModel.Hidden(chipId = PopupChipId.MediaControl) ) - private val incomingPopupChipBundle: Flow<PopupChipBundle?> = - flowOf(null).stateIn(scope, SharingStarted.Lazily, PopupChipBundle()) + private val incomingPopupChipBundle: StateFlow<PopupChipBundle?> = + mediaControlChipViewModel.chip + .map { chip -> PopupChipBundle(media = chip) } + .stateIn(scope, SharingStarted.WhileSubscribed(), PopupChipBundle()) - val popupChips: Flow<List<PopupChipModel>> = - incomingPopupChipBundle - .map { _ -> listOf(null).filterIsInstance<PopupChipModel.Shown>() } - .stateIn(scope, SharingStarted.Lazily, emptyList()) + val shownPopupChips: StateFlow<List<PopupChipModel.Shown>> = + if (StatusBarPopupChips.isEnabled) { + incomingPopupChipBundle + .map { bundle -> + listOfNotNull(bundle?.media).filterIsInstance<PopupChipModel.Shown>() + } + .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + } else { + MutableStateFlow(emptyList<PopupChipModel.Shown>()).asStateFlow() + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index e440d2728263..dd3a9c9dcf21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -169,12 +169,12 @@ public class NotificationBackgroundView extends View implements Dumpable, && !mExpandAnimationRunning) { bottom -= mClipBottomAmount; } - final boolean isRtl = isLayoutRtl(); + final boolean alignedToRight = isAlignedToRight(); final int width = getWidth(); final int actualWidth = getActualWidth(); - int left = isRtl ? width - actualWidth : 0; - int right = isRtl ? width : actualWidth; + int left = alignedToRight ? width - actualWidth : 0; + int right = alignedToRight ? width : actualWidth; if (mExpandAnimationRunning) { // Horizontally center this background view inside of the container @@ -185,6 +185,15 @@ public class NotificationBackgroundView extends View implements Dumpable, return new Rect(left, top, right, bottom); } + /** + * @return Whether the background view should be right-aligned. This only matters if the + * actualWidth is different than the full (measured) width. In other words, this is used to + * define the short-shelf alignment. + */ + protected boolean isAlignedToRight() { + return isLayoutRtl(); + } + private void draw(Canvas canvas, Drawable drawable) { NotificationAddXOnHoverToDismiss.assertInLegacyMode(); @@ -196,12 +205,13 @@ public class NotificationBackgroundView extends View implements Dumpable, && !mExpandAnimationRunning) { bottom -= mClipBottomAmount; } - final boolean isRtl = isLayoutRtl(); + + final boolean alignedToRight = isAlignedToRight(); final int width = getWidth(); final int actualWidth = getActualWidth(); - int left = isRtl ? width - actualWidth : 0; - int right = isRtl ? width : actualWidth; + int left = alignedToRight ? width - actualWidth : 0; + int right = alignedToRight ? width : actualWidth; if (mExpandAnimationRunning) { // Horizontally center this background view inside of the container diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/NotificationShelfBackgroundView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/NotificationShelfBackgroundView.kt new file mode 100644 index 000000000000..d7eea0190e2e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/NotificationShelfBackgroundView.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.shelf + +import android.content.Context +import android.util.AttributeSet +import androidx.annotation.VisibleForTesting +import com.android.systemui.statusbar.notification.row.NotificationBackgroundView +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism + +/** The background view for the NotificationShelf. */ +class NotificationShelfBackgroundView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null) : + NotificationBackgroundView(context, attrs) { + + /** Whether the notification shelf is aligned to end, need to keep persistent with the shelf. */ + var alignToEnd = false + + /** @return whether the alignment of the notification shelf is right. */ + @VisibleForTesting + public override fun isAlignedToRight(): Boolean { + if (!NotificationMinimalism.isEnabled) { + return super.isAlignedToRight() + } + return alignToEnd xor isLayoutRtl + } + + override fun toDumpString(): String { + return super.toDumpString() + " alignToEnd=" + alignToEnd + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/NotificationShelfIconContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/NotificationShelfIconContainer.kt new file mode 100644 index 000000000000..64d165402759 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/NotificationShelfIconContainer.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.shelf + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism +import com.android.systemui.statusbar.phone.NotificationIconContainer +import kotlin.math.max + +/** The NotificationIconContainer for the NotificationShelf. */ +class NotificationShelfIconContainer +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null) : + NotificationIconContainer(context, attrs) { + + /** Whether the notification shelf is aligned to end. */ + var alignToEnd = false + + /** + * @return The left boundary (not the RTL compatible start) of the area that icons can be added. + */ + override fun getLeftBound(): Float { + if (!NotificationMinimalism.isEnabled) { + return super.getLeftBound() + } + + if (isAlignedToRight) { + return (max(width - actualWidth, 0) + actualPaddingStart) + } + return actualPaddingStart + } + + /** + * @return The right boundary (not the RTL compatible end) of the area that icons can be added. + */ + override fun getRightBound(): Float { + if (!NotificationMinimalism.isEnabled) { + return super.getRightBound() + } + + if (isAlignedToRight) { + return width - actualPaddingEnd + } + return actualWidth - actualPaddingEnd + } + + /** + * For RTL, the icons' x positions should be mirrored around the middle of the shelf so that the + * icons are also added to the shelf from right to left. This function should only be called + * when RTL. + */ + override fun getRtlIconTranslationX(iconState: IconState, iconView: View): Float { + if (!NotificationMinimalism.isEnabled) { + return super.getRtlIconTranslationX(iconState, iconView) + } + + if (!isLayoutRtl) { + return iconState.xTranslation + } + + if (isAlignedToRight) { + return width * 2 - actualWidth - iconState.xTranslation - iconView.width + } + return actualWidth - iconState.xTranslation - iconView.width + } + + private val isAlignedToRight: Boolean + get() { + if (!NotificationMinimalism.isEnabled) { + return isLayoutRtl + } + return alignToEnd xor isLayoutRtl + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index ecd62bd6943b..c396512ce3a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -198,7 +198,7 @@ public class NotificationIconContainer extends ViewGroup { Paint paint = new Paint(); paint.setColor(Color.RED); paint.setStyle(Paint.Style.STROKE); - canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint); + canvas.drawRect(getActualPaddingStart(), 0, getRightBound(), getHeight(), paint); if (DEBUG_OVERFLOW) { if (mLastVisibleIconState == null) { @@ -469,11 +469,11 @@ public class NotificationIconContainer extends ViewGroup { * If this is not a whole number, the fraction means by how much the icon is appearing. */ public void calculateIconXTranslations() { - float translationX = getActualPaddingStart(); + float translationX = getLeftBound(); int firstOverflowIndex = -1; int childCount = getChildCount(); int maxVisibleIcons = mMaxIcons; - float layoutEnd = getLayoutEnd(); + float layoutRight = getRightBound(); mVisualOverflowStart = 0; mFirstVisibleIconState = null; for (int i = 0; i < childCount; i++) { @@ -495,7 +495,7 @@ public class NotificationIconContainer extends ViewGroup { final boolean forceOverflow = shouldForceOverflow(i, mSpeedBumpIndex, iconState.iconAppearAmount, maxVisibleIcons); final boolean isOverflowing = forceOverflow || isOverflowing( - /* isLastChild= */ i == childCount - 1, translationX, layoutEnd, mIconSize); + /* isLastChild= */ i == childCount - 1, translationX, layoutRight, mIconSize); // First icon to overflow. if (firstOverflowIndex == -1 && isOverflowing) { @@ -536,8 +536,7 @@ public class NotificationIconContainer extends ViewGroup { for (int i = 0; i < childCount; i++) { View view = getChildAt(i); IconState iconState = mIconStates.get(view); - iconState.setXTranslation( - getWidth() - iconState.getXTranslation() - view.getWidth()); + iconState.setXTranslation(getRtlIconTranslationX(iconState, view)); } } if (mIsolatedIcon != null) { @@ -553,6 +552,11 @@ public class NotificationIconContainer extends ViewGroup { } } + /** We need this to keep icons ordered from right to left when RTL. */ + protected float getRtlIconTranslationX(IconState iconState, View iconView) { + return getWidth() - iconState.getXTranslation() - iconView.getWidth(); + } + private float getDrawingScale(View view) { return mUseIncreasedIconScale && view instanceof StatusBarIconView ? ((StatusBarIconView) view).getIconScaleIncreased() @@ -563,11 +567,21 @@ public class NotificationIconContainer extends ViewGroup { mUseIncreasedIconScale = useIncreasedIconScale; } - private float getLayoutEnd() { + /** + * @return The right boundary (not the RTL compatible end) of the area that icons can be added. + */ + protected float getRightBound() { return getActualWidth() - getActualPaddingEnd(); } - private float getActualPaddingEnd() { + /** + * @return The left boundary (not the RTL compatible start) of the area that icons can be added. + */ + protected float getLeftBound() { + return getActualPaddingStart(); + } + + protected float getActualPaddingEnd() { if (mActualPaddingEnd == NO_VALUE) { return getPaddingEnd(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index ebf439161a9d..c3299bbd40e6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.composable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -28,13 +29,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.keyguard.AlphaOptimizedLinearLayout import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor +import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips +import com.android.systemui.statusbar.featurepods.popups.ui.compose.StatusBarPopupChipsContainer import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder import com.android.systemui.statusbar.phone.NotificationIconContainer import com.android.systemui.statusbar.phone.PhoneStatusBarView @@ -172,6 +177,35 @@ fun StatusBarRoot( R.id.notificationIcons ) + // Add a composable container for `StatusBarPopupChip`s + if (StatusBarPopupChips.isEnabled) { + val endSideContent = + phoneStatusBarView.requireViewById<AlphaOptimizedLinearLayout>( + R.id.status_bar_end_side_content + ) + + val composeView = + ComposeView(context).apply { + layoutParams = + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + ) + + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + + setContent { + val chips = + statusBarViewModel.statusBarPopupChips + .collectAsStateWithLifecycle() + StatusBarPopupChipsContainer(chips = chips.value) + } + } + endSideContent.addView(composeView, 0) + } + scope.launch { notificationIconsBinder.bindWhileAttached( notificationIconContainer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index 7f9a80b2e62f..dcfbc5d6432d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -42,6 +42,8 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsVie import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel +import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipsViewModel import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.headsup.PinnedStatus @@ -99,6 +101,9 @@ interface HomeStatusBarViewModel { /** View model for the carrier name that may show in the status bar based on carrier config */ val operatorNameViewModel: StatusBarOperatorNameViewModel + /** The popup chips that should be shown on the right-hand side of the status bar. */ + val statusBarPopupChips: StateFlow<List<PopupChipModel.Shown>> + /** * True if the current scene can show the home status bar (aka this status bar), and false if * the current scene should never show the home status bar. @@ -170,6 +175,7 @@ constructor( sceneContainerOcclusionInteractor: SceneContainerOcclusionInteractor, shadeInteractor: ShadeInteractor, ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, + statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel, animations: SystemStatusEventAnimationInteractor, @Application coroutineScope: CoroutineScope, ) : HomeStatusBarViewModel { @@ -188,6 +194,8 @@ constructor( override val ongoingActivityChips = ongoingActivityChipsViewModel.chips + override val statusBarPopupChips = statusBarPopupChipsViewModel.shownPopupChips + override val isHomeStatusBarAllowedByScene: StateFlow<Boolean> = combine( sceneInteractor.currentScene, diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt index fbf7072cc0a0..a6c066500054 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt @@ -31,6 +31,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker import com.android.systemui.touchpad.tutorial.ui.gesture.VerticalVelocityTracker import com.android.systemui.touchpad.tutorial.ui.view.TouchpadTutorialActivity import com.android.systemui.touchpad.tutorial.ui.viewmodel.BackGestureScreenViewModel +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.HomeGestureScreenViewModel import dagger.Binds import dagger.Module @@ -53,7 +54,11 @@ interface TouchpadTutorialModule { backGestureScreenViewModel: BackGestureScreenViewModel, homeGestureScreenViewModel: HomeGestureScreenViewModel, ): TouchpadTutorialScreensProvider { - return ScreensProvider(backGestureScreenViewModel, homeGestureScreenViewModel) + return ScreensProvider( + backGestureScreenViewModel, + homeGestureScreenViewModel, + EasterEggGestureViewModel(), + ) } @SysUISingleton @@ -74,14 +79,25 @@ interface TouchpadTutorialModule { private class ScreensProvider( val backGestureScreenViewModel: BackGestureScreenViewModel, val homeGestureScreenViewModel: HomeGestureScreenViewModel, + val easterEggGestureViewModel: EasterEggGestureViewModel, ) : TouchpadTutorialScreensProvider { @Composable override fun BackGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { - BackGestureTutorialScreen(backGestureScreenViewModel, onDoneButtonClicked, onBack) + BackGestureTutorialScreen( + backGestureScreenViewModel, + easterEggGestureViewModel, + onDoneButtonClicked, + onBack, + ) } @Composable override fun HomeGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { - HomeGestureTutorialScreen(homeGestureScreenViewModel, onDoneButtonClicked, onBack) + HomeGestureTutorialScreen( + homeGestureScreenViewModel, + easterEggGestureViewModel, + onDoneButtonClicked, + onBack, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index 804a764b5349..ae32b7a6175c 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -25,10 +25,12 @@ import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenCon import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.viewmodel.BackGestureScreenViewModel +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel @Composable fun BackGestureTutorialScreen( viewModel: BackGestureScreenViewModel, + easterEggGestureViewModel: EasterEggGestureViewModel, onDoneButtonClicked: () -> Unit, onBack: () -> Unit, ) { @@ -49,9 +51,12 @@ fun BackGestureTutorialScreen( GestureTutorialScreen( screenConfig = screenConfig, gestureUiStateFlow = viewModel.gestureUiState, - motionEventConsumer = viewModel::handleEvent, - easterEggTriggeredFlow = viewModel.easterEggTriggered, - onEasterEggFinished = viewModel::onEasterEggFinished, + motionEventConsumer = { + easterEggGestureViewModel.accept(it) + viewModel.handleEvent(it) + }, + easterEggTriggeredFlow = easterEggGestureViewModel.easterEggTriggered, + onEasterEggFinished = easterEggGestureViewModel::onEasterEggFinished, onDoneButtonClicked = onDoneButtonClicked, onBack = onBack, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt index 5dcd788ea4fd..4f1f40dc4c05 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -23,11 +23,13 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.HomeGestureScreenViewModel @Composable fun HomeGestureTutorialScreen( viewModel: HomeGestureScreenViewModel, + easterEggGestureViewModel: EasterEggGestureViewModel, onDoneButtonClicked: () -> Unit, onBack: () -> Unit, ) { @@ -48,9 +50,12 @@ fun HomeGestureTutorialScreen( GestureTutorialScreen( screenConfig = screenConfig, gestureUiStateFlow = viewModel.gestureUiState, - motionEventConsumer = viewModel::handleEvent, - easterEggTriggeredFlow = viewModel.easterEggTriggered, - onEasterEggFinished = viewModel::onEasterEggFinished, + motionEventConsumer = { + easterEggGestureViewModel.accept(it) + viewModel.handleEvent(it) + }, + easterEggTriggeredFlow = easterEggGestureViewModel.easterEggTriggered, + onEasterEggFinished = easterEggGestureViewModel::onEasterEggFinished, onDoneButtonClicked = onDoneButtonClicked, onBack = onBack, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt index 7ff838981950..6c9e26c4b7ea 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt @@ -23,11 +23,13 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.RecentAppsGestureScreenViewModel @Composable fun RecentAppsGestureTutorialScreen( viewModel: RecentAppsGestureScreenViewModel, + easterEggGestureViewModel: EasterEggGestureViewModel, onDoneButtonClicked: () -> Unit, onBack: () -> Unit, ) { @@ -49,9 +51,12 @@ fun RecentAppsGestureTutorialScreen( GestureTutorialScreen( screenConfig = screenConfig, gestureUiStateFlow = viewModel.gestureUiState, - motionEventConsumer = viewModel::handleEvent, - easterEggTriggeredFlow = viewModel.easterEggTriggered, - onEasterEggFinished = viewModel::onEasterEggFinished, + motionEventConsumer = { + easterEggGestureViewModel.accept(it) + viewModel.handleEvent(it) + }, + easterEggTriggeredFlow = easterEggGestureViewModel.easterEggTriggered, + onEasterEggFinished = easterEggGestureViewModel::onEasterEggFinished, onDoneButtonClicked = onDoneButtonClicked, onBack = onBack, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureRecognizer.kt index 7483840d1933..18c490baed3d 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureRecognizer.kt @@ -17,7 +17,7 @@ package com.android.systemui.touchpad.tutorial.ui.gesture import android.view.MotionEvent -import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor.Companion.CIRCLES_COUNT_THRESHOLD +import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureRecognizer.Companion.CIRCLES_COUNT_THRESHOLD import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.pow @@ -25,10 +25,12 @@ import kotlin.math.sqrt /** * Monitor recognizing easter egg gesture, that is at least [CIRCLES_COUNT_THRESHOLD] circles - * clockwise within one gesture. It tries to be on the safer side of not triggering gesture if we're - * not sure if full circle was done. + * clockwise within one two-fingers gesture. It tries to be on the safer side of not triggering + * gesture if we're not sure if full circle was done. */ -class EasterEggGestureMonitor(private val callback: () -> Unit) { +class EasterEggGestureRecognizer : GestureRecognizer { + + private var gestureStateChangedCallback: (GestureState) -> Unit = {} private var last: Point = Point(0f, 0f) private var cumulativeAngle: Float = 0f @@ -39,7 +41,16 @@ class EasterEggGestureMonitor(private val callback: () -> Unit) { private val points = mutableListOf<Point>() - fun processTouchpadEvent(event: MotionEvent) { + override fun addGestureStateCallback(callback: (GestureState) -> Unit) { + gestureStateChangedCallback = callback + } + + override fun clearGestureStateCallback() { + gestureStateChangedCallback = {} + } + + override fun accept(event: MotionEvent) { + if (!isTwoFingerSwipe(event)) return when (event.action) { MotionEvent.ACTION_DOWN -> { reset() @@ -75,7 +86,7 @@ class EasterEggGestureMonitor(private val callback: () -> Unit) { // without checking if gesture is circular we can have gesture doing arches back and // forth that finally reaches full circle angle if (circleCount >= CIRCLES_COUNT_THRESHOLD && wasGestureCircular(points)) { - callback() + gestureStateChangedCallback(GestureState.Finished) } reset() } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadEventsFilter.kt index dd275bd11d1e..bddeb0b25ec2 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadEventsFilter.kt @@ -18,33 +18,27 @@ package com.android.systemui.touchpad.tutorial.ui.gesture import android.view.InputDevice import android.view.MotionEvent -import java.util.function.Consumer +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.BUTTON_PRIMARY -/** - * Allows listening to touchpadGesture and calling onDone when gesture was triggered. Can have all - * motion events passed to [onMotionEvent] and will filter touchpad events accordingly - */ -class TouchpadGestureHandler( - private val gestureRecognizer: Consumer<MotionEvent>, - private val easterEggGestureMonitor: EasterEggGestureMonitor, -) { +object TouchpadEventsFilter { - fun onMotionEvent(event: MotionEvent): Boolean { + fun isTouchpadAndNonClickEvent(event: MotionEvent): Boolean { // events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons val isFromTouchpad = event.isFromSource(InputDevice.SOURCE_MOUSE) && event.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER - val buttonClick = - event.actionMasked == MotionEvent.ACTION_DOWN && - event.isButtonPressed(MotionEvent.BUTTON_PRIMARY) - return if (isFromTouchpad && !buttonClick) { - if (isTwoFingerSwipe(event)) { - easterEggGestureMonitor.processTouchpadEvent(event) - } - gestureRecognizer.accept(event) - true - } else { - false - } + val isButtonClicked = + event.actionMasked == ACTION_DOWN && event.isButtonPressed(BUTTON_PRIMARY) + return isFromTouchpad && !isButtonClicked + } +} + +fun GestureRecognizer.handleTouchpadMotionEvent(event: MotionEvent): Boolean { + return if (TouchpadEventsFilter.isTouchpadAndNonClickEvent(event)) { + this.accept(event) + true + } else { + false } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt index 6b4cbab3ae09..cefe382a299c 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt @@ -39,6 +39,7 @@ import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialS import com.android.systemui.touchpad.tutorial.ui.composable.RecentAppsGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.TutorialSelectionScreen import com.android.systemui.touchpad.tutorial.ui.viewmodel.BackGestureScreenViewModel +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.HomeGestureScreenViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.RecentAppsGestureScreenViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.BACK_GESTURE @@ -73,6 +74,7 @@ constructor( backGestureViewModel, homeGestureViewModel, recentAppsGestureViewModel, + EasterEggGestureViewModel(), closeTutorial = ::finishTutorial, ) } @@ -105,6 +107,7 @@ fun TouchpadTutorialScreen( backGestureViewModel: BackGestureScreenViewModel, homeGestureViewModel: HomeGestureScreenViewModel, recentAppsGestureViewModel: RecentAppsGestureScreenViewModel, + easterEggGestureViewModel: EasterEggGestureViewModel, closeTutorial: () -> Unit, ) { val activeScreen by vm.screen.collectAsStateWithLifecycle(STARTED) @@ -130,18 +133,21 @@ fun TouchpadTutorialScreen( BACK_GESTURE -> BackGestureTutorialScreen( backGestureViewModel, + easterEggGestureViewModel, onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, onBack = { vm.goTo(TUTORIAL_SELECTION) }, ) HOME_GESTURE -> HomeGestureTutorialScreen( homeGestureViewModel, + easterEggGestureViewModel, onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, onBack = { vm.goTo(TUTORIAL_SELECTION) }, ) RECENT_APPS_GESTURE -> RecentAppsGestureTutorialScreen( recentAppsGestureViewModel, + easterEggGestureViewModel, onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, onBack = { vm.goTo(TUTORIAL_SELECTION) }, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt index 0154c910be91..93e8d313edcf 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt @@ -22,17 +22,15 @@ import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureRecognizer -import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress -import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler +import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent import com.android.systemui.util.kotlin.pairwiseBy import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -40,10 +38,7 @@ class BackGestureScreenViewModel @Inject constructor(configurationInteractor: ConfigurationInteractor) : TouchpadTutorialScreenViewModel { - private val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered.value = true } - override val easterEggTriggered = MutableStateFlow(false) - - private var handler: TouchpadGestureHandler? = null + private var recognizer: BackGestureRecognizer? = null private val distanceThreshold: Flow<Int> = configurationInteractor @@ -54,16 +49,15 @@ constructor(configurationInteractor: ConfigurationInteractor) : TouchpadTutorial override val gestureUiState: Flow<GestureUiState> = distanceThreshold .flatMapLatest { - val recognizer = BackGestureRecognizer(gestureDistanceThresholdPx = it) - handler = TouchpadGestureHandler(recognizer, easterEggMonitor) - GestureFlowAdapter(recognizer).gestureStateAsFlow + recognizer = BackGestureRecognizer(gestureDistanceThresholdPx = it) + GestureFlowAdapter(recognizer!!).gestureStateAsFlow } .pairwiseBy(GestureState.NotStarted) { previous, current -> toGestureUiState(current, previous) } override fun handleEvent(event: MotionEvent): Boolean { - return handler?.onMotionEvent(event) ?: false + return recognizer?.handleTouchpadMotionEvent(event) ?: false } private fun toGestureUiState(current: GestureState, previous: GestureState): GestureUiState { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModel.kt new file mode 100644 index 000000000000..69cdab6108ab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModel.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.touchpad.tutorial.ui.viewmodel + +import android.view.MotionEvent +import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureRecognizer +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState +import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent +import java.util.function.Consumer +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.receiveAsFlow + +class EasterEggGestureViewModel( + private val gestureRecognizer: EasterEggGestureRecognizer = EasterEggGestureRecognizer() +) : Consumer<MotionEvent> { + + private val gestureDone = + GestureFlowAdapter(gestureRecognizer).gestureStateAsFlow.filter { + it == GestureState.Finished + } + + private val easterEggFinished = Channel<Unit>() + + val easterEggTriggered = + merge( + gestureDone.map { Event.GestureFinished }, + easterEggFinished.receiveAsFlow().map { Event.StateRestarted }, + ) + .map { + when (it) { + Event.GestureFinished -> true + Event.StateRestarted -> false + } + } + .onStart { emit(false) } + + override fun accept(event: MotionEvent) { + gestureRecognizer.handleTouchpadMotionEvent(event) + } + + fun onEasterEggFinished() { + easterEggFinished.trySend(Unit) + } + + private sealed interface Event { + data object GestureFinished : Event + + data object StateRestarted : Event + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt index 1c865f57b8c7..9a817d810bba 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt @@ -23,17 +23,15 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState -import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureRecognizer -import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker import com.android.systemui.touchpad.tutorial.ui.gesture.VerticalVelocityTracker +import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -47,10 +45,7 @@ constructor( val velocityTracker: VelocityTracker = VerticalVelocityTracker(), ) : TouchpadTutorialScreenViewModel { - private val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered.value = true } - override val easterEggTriggered = MutableStateFlow(false) - - private var handler: TouchpadGestureHandler? = null + private var recognizer: HomeGestureRecognizer? = null private val distanceThreshold: Flow<Int> = configurationInteractor @@ -67,14 +62,13 @@ constructor( distanceThreshold .combine(velocityThreshold, { distance, velocity -> distance to velocity }) .flatMapLatest { (distance, velocity) -> - val recognizer = + recognizer = HomeGestureRecognizer( gestureDistanceThresholdPx = distance, velocityThresholdPxPerMs = velocity, velocityTracker = velocityTracker, ) - handler = TouchpadGestureHandler(recognizer, easterEggMonitor) - GestureFlowAdapter(recognizer).gestureStateAsFlow + GestureFlowAdapter(recognizer!!).gestureStateAsFlow } .map { toGestureUiState(it) } @@ -86,6 +80,6 @@ constructor( ) override fun handleEvent(event: MotionEvent): Boolean { - return handler?.onMotionEvent(event) ?: false + return recognizer?.handleTouchpadMotionEvent(event) ?: false } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt index 09947a8b109e..8215078c346d 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt @@ -23,17 +23,15 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState -import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureRecognizer -import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker import com.android.systemui.touchpad.tutorial.ui.gesture.VerticalVelocityTracker +import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -47,10 +45,7 @@ constructor( private val velocityTracker: VelocityTracker = VerticalVelocityTracker(), ) : TouchpadTutorialScreenViewModel { - private val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered.value = true } - override val easterEggTriggered = MutableStateFlow(false) - - private var handler: TouchpadGestureHandler? = null + private var recognizer: RecentAppsGestureRecognizer? = null private val distanceThreshold: Flow<Int> = configurationInteractor.onAnyConfigurationChange @@ -71,14 +66,13 @@ constructor( distanceThreshold .combine(velocityThreshold, { distance, velocity -> distance to velocity }) .flatMapLatest { (distance, velocity) -> - val recognizer = + recognizer = RecentAppsGestureRecognizer( gestureDistanceThresholdPx = distance, velocityThresholdPxPerMs = velocity, velocityTracker = velocityTracker, ) - handler = TouchpadGestureHandler(recognizer, easterEggMonitor) - GestureFlowAdapter(recognizer).gestureStateAsFlow + GestureFlowAdapter(recognizer!!).gestureStateAsFlow } .map { toGestureUiState(it) } @@ -90,6 +84,6 @@ constructor( ) override fun handleEvent(event: MotionEvent): Boolean { - return handler?.onMotionEvent(event) ?: false + return recognizer?.handleTouchpadMotionEvent(event) ?: false } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt index 500f6a0238c3..31e953d6643c 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt @@ -19,15 +19,9 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel import android.view.MotionEvent import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow interface TouchpadTutorialScreenViewModel { val gestureUiState: Flow<GestureUiState> - val easterEggTriggered: MutableStateFlow<Boolean> - - fun onEasterEggFinished() { - easterEggTriggered.value = false - } fun handleEvent(event: MotionEvent): Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index a7ffcd747a6b..67ffb0602860 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -16,18 +16,15 @@ package com.android.systemui.volume.dialog.sliders.ui -import android.animation.Animator -import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.view.View -import android.view.animation.DecelerateInterpolator +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce import com.android.systemui.res.R import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderStateModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel -import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory -import com.android.systemui.volume.dialog.ui.utils.suspendAnimate -import com.google.android.material.slider.LabelFormatter import com.google.android.material.slider.Slider import javax.inject.Inject import kotlin.math.roundToInt @@ -35,53 +32,61 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -private const val PROGRESS_CHANGE_ANIMATION_DURATION_MS = 80L - @VolumeDialogSliderScope class VolumeDialogSliderViewBinder @Inject -constructor( - private val viewModel: VolumeDialogSliderViewModel, - private val jankListenerFactory: JankListenerFactory, -) { +constructor(private val viewModel: VolumeDialogSliderViewModel) { - fun CoroutineScope.bind(view: View) { - val sliderView: Slider = - view.requireViewById<Slider>(R.id.volume_dialog_slider).apply { - labelBehavior = LabelFormatter.LABEL_GONE - trackIconActiveColor = trackInactiveTintList + private val sliderValueProperty = + object : FloatPropertyCompat<Slider>("value") { + override fun getValue(slider: Slider): Float = slider.value + + override fun setValue(slider: Slider, value: Float) { + slider.value = value } + } + private val springForce = + SpringForce().apply { + stiffness = SpringForce.STIFFNESS_MEDIUM + dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + } + + fun CoroutineScope.bind(view: View) { + var isInitialUpdate = true + val sliderView: Slider = view.requireViewById(R.id.volume_dialog_slider) + val animation = SpringAnimation(sliderView, sliderValueProperty) + animation.spring = springForce + sliderView.addOnChangeListener { _, value, fromUser -> viewModel.setStreamVolume(value.roundToInt(), fromUser) } - viewModel.state.onEach { sliderView.setModel(it) }.launchIn(this) + viewModel.state + .onEach { + sliderView.setModel(it, animation, isInitialUpdate) + isInitialUpdate = false + } + .launchIn(this) } @SuppressLint("UseCompatLoadingForDrawables") - private suspend fun Slider.setModel(model: VolumeDialogSliderStateModel) { + private fun Slider.setModel( + model: VolumeDialogSliderStateModel, + animation: SpringAnimation, + isInitialUpdate: Boolean, + ) { valueFrom = model.minValue + animation.setMinValue(model.minValue) valueTo = model.maxValue + animation.setMaxValue(model.maxValue) // coerce the current value to the new value range before animating it. This prevents // animating from the value that is outside of current [valueFrom, valueTo]. value = value.coerceIn(valueFrom, valueTo) - setValueAnimated( - model.value, - jankListenerFactory.update(this, PROGRESS_CHANGE_ANIMATION_DURATION_MS), - ) - trackIconActiveEnd = context.getDrawable(model.iconRes) - } -} - -private suspend fun Slider.setValueAnimated( - newValue: Float, - jankListener: Animator.AnimatorListener, -) { - ObjectAnimator.ofFloat(value, newValue) - .apply { - duration = PROGRESS_CHANGE_ANIMATION_DURATION_MS - interpolator = DecelerateInterpolator() - addListener(jankListener) + setTrackIconActiveStart(model.iconRes) + if (isInitialUpdate) { + value = model.value + } else { + animation.animateToFinalPosition(model.value) } - .suspendAnimate<Float> { value = it } + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt index 2d5652420ec8..6d8457be1014 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt @@ -73,19 +73,22 @@ constructor( .filterNotNull() val state: Flow<VolumeDialogSliderStateModel> = - model.flatMapLatest { streamModel -> - with(streamModel) { - volumeDialogSliderIconProvider.getStreamIcon( - stream = stream, - level = level, - levelMin = levelMin, - levelMax = levelMax, - isMuted = muted, - isRoutedToBluetooth = routedToBluetooth, - ) - } - .map { icon -> streamModel.toStateModel(icon) } - } + model + .flatMapLatest { streamModel -> + with(streamModel) { + volumeDialogSliderIconProvider.getStreamIcon( + stream = stream, + level = level, + levelMin = levelMin, + levelMax = levelMax, + isMuted = muted, + isRoutedToBluetooth = routedToBluetooth, + ) + } + .map { icon -> streamModel.toStateModel(icon) } + } + .stateIn(coroutineScope, SharingStarted.Eagerly, null) + .filterNotNull() init { userVolumeUpdates diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt index 9b1d86f15c92..52a19e0903e2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.dialog.ui.utils import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener import android.view.ViewPropertyAnimator import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringAnimation @@ -69,17 +70,25 @@ suspend fun ViewPropertyAnimator.suspendAnimate( @Suppress("UNCHECKED_CAST") suspend fun <T> ValueAnimator.suspendAnimate(onValueChanged: (T) -> Unit) { suspendCancellableCoroutine { continuation -> - addListener( + val listener = object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) = continuation.resumeIfCan(Unit) + override fun onAnimationEnd(animation: Animator) = + continuation.resumeIfCan(Unit) - override fun onAnimationCancel(animation: Animator) = continuation.resumeIfCan(Unit) - } - ) - addUpdateListener { onValueChanged(it.animatedValue as T) } + override fun onAnimationCancel(animation: Animator) = + continuation.resumeIfCan(Unit) + } + .also(::addListener) + val updateListener = + AnimatorUpdateListener { onValueChanged(it.animatedValue as T) } + .also(::addUpdateListener) start() - continuation.invokeOnCancellation { cancel() } + continuation.invokeOnCancellation { + removeUpdateListener(updateListener) + removeListener(listener) + cancel() + } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/FakeMediaControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/FakeMediaControllerFactory.kt index b833750a2c4a..b20678e95a86 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/FakeMediaControllerFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/FakeMediaControllerFactory.kt @@ -35,7 +35,7 @@ class FakeMediaControllerFactory(context: Context) : MediaControllerFactory(cont return mediaControllersForToken[token]!! } - override suspend fun create(token: SessionToken, looper: Looper): Media3Controller { + override suspend fun create(token: SessionToken, looper: Looper): Media3Controller? { return media3Controller ?: super.create(token, looper) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorKosmos.kt index 0025ad42ba53..f7e235a9c749 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorKosmos.kt @@ -17,13 +17,13 @@ package com.android.systemui.statusbar.featurepods.media.domain.interactor import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.backgroundScope import com.android.systemui.media.controls.data.repository.mediaFilterRepository val Kosmos.mediaControlChipInteractor: MediaControlChipInteractor by Kosmos.Fixture { MediaControlChipInteractor( - applicationScope = applicationCoroutineScope, + backgroundScope = backgroundScope, mediaFilterRepository = mediaFilterRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt new file mode 100644 index 000000000000..7145907a14a8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.media.ui.viewmodel + +import android.content.testableContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor + +val Kosmos.mediaControlChipViewModel: MediaControlChipViewModel by + Kosmos.Fixture { + MediaControlChipViewModel( + backgroundScope = applicationCoroutineScope, + applicationContext = testableContext, + mediaControlChipInteractor = mediaControlChipInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt index 62cdc87f980f..93502f365202 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt @@ -18,6 +18,12 @@ package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.mediaControlChipViewModel val Kosmos.statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel by - Kosmos.Fixture { StatusBarPopupChipsViewModel(testScope.backgroundScope) } + Kosmos.Fixture { + StatusBarPopupChipsViewModel( + testScope.backgroundScope, + mediaControlChipViewModel = mediaControlChipViewModel, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt index 924b6b43b49a..b38a723f1fa7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt @@ -25,6 +25,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor +import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModel import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.phone.domain.interactor.darkIconInteractor @@ -48,6 +49,7 @@ var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by sceneContainerOcclusionInteractor, shadeInteractor, ongoingActivityChipsViewModel, + statusBarPopupChipsViewModel, systemStatusEventAnimationInteractor, applicationCoroutineScope, ) diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java index 676c6fa4f138..3660607d8764 100644 --- a/services/core/java/com/android/server/pm/BroadcastHelper.java +++ b/services/core/java/com/android/server/pm/BroadcastHelper.java @@ -360,13 +360,17 @@ public final class BroadcastHelper { @Nullable SparseArray<int[]> broadcastAllowList, @NonNull AndroidPackage pkg, @NonNull String[] sharedUidPackages, - @NonNull String reasonForTrace) { + @NonNull String reasonForTrace, + int callingUidForTrace) { final boolean isForWholeApp = componentNames.contains(packageName); + final String callingPackageNameForTrace = mContext.getPackageManager().getNameForUid( + callingUidForTrace); if (isForWholeApp || !android.content.pm.Flags.reduceBroadcastsForComponentStateChanges()) { tracePackageChangedBroadcastEvent( android.content.pm.Flags.reduceBroadcastsForComponentStateChanges(), reasonForTrace, packageName, "<implicit>" /* targetPackageName */, - "whole" /* targetComponent */, componentNames.size()); + "whole" /* targetComponent */, componentNames.size(), + callingPackageNameForTrace); sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, componentNames, packageUid, reason, userIds, instantUserIds, broadcastAllowList, null /* targetPackageName */, null /* requiredPermissions */); @@ -390,7 +394,7 @@ public final class BroadcastHelper { if (!TextUtils.equals(packageName, "android")) { tracePackageChangedBroadcastEvent(true /* applyFlag */, reasonForTrace, packageName, "android" /* targetPackageName */, "notExported" /* targetComponent */, - notExportedComponentNames.size()); + notExportedComponentNames.size(), callingPackageNameForTrace); sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, notExportedComponentNames, packageUid, reason, userIds, instantUserIds, broadcastAllowList, "android" /* targetPackageName */, @@ -401,7 +405,7 @@ public final class BroadcastHelper { // Second, send the PACKAGE_CHANGED broadcast to the application itself. tracePackageChangedBroadcastEvent(true /* applyFlag */, reasonForTrace, packageName, packageName /* targetPackageName */, "notExported" /* targetComponent */, - notExportedComponentNames.size()); + notExportedComponentNames.size(), callingPackageNameForTrace); sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, notExportedComponentNames, packageUid, reason, userIds, instantUserIds, broadcastAllowList, packageName /* targetPackageName */, @@ -415,7 +419,7 @@ public final class BroadcastHelper { } tracePackageChangedBroadcastEvent(true /* applyFlag */, reasonForTrace, packageName, sharedPackage /* targetPackageName */, "notExported" /* targetComponent */, - notExportedComponentNames.size()); + notExportedComponentNames.size(), callingPackageNameForTrace); sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, notExportedComponentNames, packageUid, reason, userIds, instantUserIds, broadcastAllowList, sharedPackage /* targetPackageName */, @@ -427,7 +431,7 @@ public final class BroadcastHelper { if (!exportedComponentNames.isEmpty()) { tracePackageChangedBroadcastEvent(true /* applyFlag */, reasonForTrace, packageName, "<implicit>" /* targetPackageName */, "exported" /* targetComponent */, - exportedComponentNames.size()); + exportedComponentNames.size(), callingPackageNameForTrace); sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, exportedComponentNames, packageUid, reason, userIds, instantUserIds, broadcastAllowList, null /* targetPackageName */, @@ -770,7 +774,8 @@ public final class BroadcastHelper { dontKillApp, new ArrayList<>(Collections.singletonList(pkg.getPackageName())), pkg.getUid(), null /* reason */, - "static_shared_library_changed" /* reasonForTrace */); + "static_shared_library_changed" /* reasonForTrace */, + Process.SYSTEM_UID); } } } @@ -962,7 +967,8 @@ public final class BroadcastHelper { @NonNull ArrayList<String> componentNames, int packageUid, @NonNull String reason, - @NonNull String reasonForTrace) { + @NonNull String reasonForTrace, + int callingUidForTrace) { PackageStateInternal setting = snapshot.getPackageStateInternal(packageName, Process.SYSTEM_UID); if (setting == null || setting.getPkg() == null) { @@ -980,7 +986,7 @@ public final class BroadcastHelper { mHandler.post(() -> sendPackageChangedBroadcastInternal( packageName, dontKillApp, componentNames, packageUid, reason, userIds, instantUserIds, broadcastAllowList, setting.getPkg(), - sharedUserPackages, reasonForTrace)); + sharedUserPackages, reasonForTrace, callingUidForTrace)); mPackageMonitorCallbackHelper.notifyPackageChanged(packageName, dontKillApp, componentNames, packageUid, reason, userIds, instantUserIds, broadcastAllowList, mHandler); } @@ -1277,7 +1283,7 @@ public final class BroadcastHelper { private static void tracePackageChangedBroadcastEvent(boolean applyFlag, @NonNull String reasonForTrace, @Nullable String packageName, @Nullable String targetPackageName, @Nullable String targetComponent, - int componentSize) { + int componentSize, @Nullable String callingPackageNameForTrace) { if (!Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { return; @@ -1291,6 +1297,7 @@ public final class BroadcastHelper { builder.append(",tpn="); builder.append(targetPackageName); builder.append(",tc="); builder.append(targetComponent); builder.append(",cs="); builder.append(componentSize); + builder.append(",cpnft="); builder.append(callingPackageNameForTrace); Trace.instant(Trace.TRACE_TAG_SYSTEM_SERVER, builder.toString()); } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index b48b39c2edd5..85b92c79403a 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2983,7 +2983,7 @@ final class InstallPackageHelper { } } - public void sendPendingBroadcasts(String reasonForTrace) { + public void sendPendingBroadcasts(String reasonForTrace, int callingUidForTrace) { String[] packages; ArrayList<String>[] components; int numBroadcasts = 0, numUsers; @@ -3028,7 +3028,7 @@ final class InstallPackageHelper { for (int i = 0; i < numBroadcasts; i++) { mBroadcastHelper.sendPackageChangedBroadcast(snapshot, packages[i], true /* dontKillApp */, components[i], uids[i], null /* reason */, - reasonForTrace); + reasonForTrace, callingUidForTrace); } } diff --git a/services/core/java/com/android/server/pm/PackageHandler.java b/services/core/java/com/android/server/pm/PackageHandler.java index 0a067048be42..bc03b10b41b4 100644 --- a/services/core/java/com/android/server/pm/PackageHandler.java +++ b/services/core/java/com/android/server/pm/PackageHandler.java @@ -76,7 +76,7 @@ final class PackageHandler extends Handler { void doHandleMessage(Message msg) { switch (msg.what) { case SEND_PENDING_BROADCAST: { - mPm.sendPendingBroadcasts((String) msg.obj); + mPm.sendPendingBroadcasts((String) msg.obj, msg.arg1); break; } case POST_INSTALL: { diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index a0bbc454c10b..aaa4fdf12411 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -3503,7 +3503,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService * if the resetEnabledSettingsOnAppDataCleared is {@code true}. */ @GuardedBy("mLock") - private void resetComponentEnabledSettingsIfNeededLPw(String packageName, int userId) { + private void resetComponentEnabledSettingsIfNeededLPw(String packageName, int userId, + int callingUid) { final AndroidPackage pkg = packageName != null ? mPackages.get(packageName) : null; if (pkg == null || !pkg.isResetEnabledSettingsOnAppDataCleared()) { return; @@ -3542,9 +3543,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService mPendingBroadcasts.addComponents(userId, packageName, updatedComponents); if (!mHandler.hasMessages(SEND_PENDING_BROADCAST)) { mHandler.sendMessageDelayed( - mHandler.obtainMessage(SEND_PENDING_BROADCAST, - "reset_component_state_changed" /* obj */), - BROADCAST_DELAY); + mHandler.obtainMessage(SEND_PENDING_BROADCAST, callingUid, 0 /* arg2 */, + "reset_component_state_changed" /* obj */), BROADCAST_DELAY); } } @@ -3841,8 +3841,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService mPendingBroadcasts.addComponent(userId, componentPkgName, componentName.getClassName()); if (!mHandler.hasMessages(SEND_PENDING_BROADCAST)) { - mHandler.sendMessageDelayed(mHandler.obtainMessage(SEND_PENDING_BROADCAST, - "component_label_icon_changed" /* obj */), BROADCAST_DELAY); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(SEND_PENDING_BROADCAST, callingUid, 0 /* arg2 */, + "component_label_icon_changed" /* obj */), BROADCAST_DELAY); } } @@ -4101,8 +4102,10 @@ public class PackageManagerService implements PackageSender, TestUtilityService final long broadcastDelay = SystemClock.uptimeMillis() > mServiceStartWithDelay ? BROADCAST_DELAY : BROADCAST_DELAY_DURING_STARTUP; - mHandler.sendMessageDelayed(mHandler.obtainMessage(SEND_PENDING_BROADCAST, - "component_state_changed" /* obj */), broadcastDelay); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(SEND_PENDING_BROADCAST, callingUid, + 0 /* arg2 */, "component_state_changed" /* obj */), + broadcastDelay); } } } @@ -4121,7 +4124,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService userId, pkgSettings.get(packageName).getAppId()); mBroadcastHelper.sendPackageChangedBroadcast(newSnapshot, packageName, false /* dontKillApp */, components, packageUid, null /* reason */, - "component_state_changed" /* reasonForTrace */); + "component_state_changed" /* reasonForTrace */, callingUid); } } finally { Binder.restoreCallingIdentity(callingId); @@ -4349,7 +4352,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService true /* dontKillApp */, new ArrayList<>(Collections.singletonList(pkg.getPackageName())), pkg.getUid(), - Intent.ACTION_OVERLAY_CHANGED, "overlay_changed" /* reasonForTrace */); + Intent.ACTION_OVERLAY_CHANGED, "overlay_changed" /* reasonForTrace */, + Process.SYSTEM_UID); } }, overlayFilter); @@ -4847,7 +4851,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService mInstantAppRegistry.deleteInstantApplicationMetadata(packageName, userId); synchronized (mLock) { if (succeeded) { - resetComponentEnabledSettingsIfNeededLPw(packageName, userId); + resetComponentEnabledSettingsIfNeededLPw(packageName, userId, + callingUid); } } } @@ -6357,7 +6362,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService @Override public void setMimeGroup(String packageName, String mimeGroup, List<String> mimeTypes) { final Computer snapshot = snapshotComputer(); - enforceOwnerRights(snapshot, packageName, Binder.getCallingUid()); + final int callingUid = Binder.getCallingUid(); + enforceOwnerRights(snapshot, packageName, callingUid); mimeTypes = CollectionUtils.emptyIfNull(mimeTypes); for (int i = 0; i < mimeTypes.size(); i++) { if (mimeTypes.get(i).length() > 255) { @@ -6401,7 +6407,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService final int packageUid = UserHandle.getUid(userIds[i], appId); mBroadcastHelper.sendPackageChangedBroadcast(snapShot, packageName, true /* dontKillApp */, components, packageUid, reason, - "mime_group_changed" /* reasonForTrace */); + "mime_group_changed" /* reasonForTrace */, callingUid); } } }); @@ -8196,8 +8202,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService mRemovePackageHelper.cleanUpForMoveInstall(volumeUuid, packageName, fromCodePath); } - void sendPendingBroadcasts(String reasonForTrace) { - mInstallPackageHelper.sendPendingBroadcasts(reasonForTrace); + void sendPendingBroadcasts(String reasonForTrace, int callingUidForTrace) { + mInstallPackageHelper.sendPendingBroadcasts(reasonForTrace, callingUidForTrace); } void handlePackagePostInstall(@NonNull InstallRequest request, boolean launchedForRestore) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b71256d27a14..e4ad56fdc074 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -225,6 +225,7 @@ import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_F import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE; import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE; import static com.android.server.wm.ActivityTaskManagerService.getInputDispatchingTimeoutMillisLocked; +import static com.android.server.wm.ActivityTaskManagerService.isPip2ExperimentEnabled; import static com.android.server.wm.IdentifierProto.HASH_CODE; import static com.android.server.wm.IdentifierProto.TITLE; import static com.android.server.wm.IdentifierProto.USER_ID; @@ -1495,7 +1496,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // precede the configuration change from the resize.) mLastReportedPictureInPictureMode = inPictureInPictureMode; mLastReportedMultiWindowMode = inPictureInPictureMode; - ensureActivityConfiguration(true /* ignoreVisibility */); + if (!isPip2ExperimentEnabled()) { + // PiP2 should handle sending out the configuration as a part of Shell Transitions. + ensureActivityConfiguration(true /* ignoreVisibility */); + } if (inPictureInPictureMode && findMainWindow() == null && task.topRunningActivity() == this) { // Prevent malicious app entering PiP without valid WindowState, which can in turn diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java index cfd324830db5..26b7cc67876e 100644 --- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java +++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java @@ -34,6 +34,7 @@ import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; +import com.android.window.flags.Flags; import java.io.File; import java.io.PrintWriter; @@ -498,50 +499,73 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord } final TaskFragment currTF = currentActivity.getTaskFragment(); final TaskFragment prevTF = initPrev.getTaskFragment(); - final TaskFragment prevAdjacentTF = prevTF != null - ? prevTF.getAdjacentTaskFragment() : null; - if (currTF == prevTF && currTF != null || prevAdjacentTF == null) { - // Current activity and previous one is in the same task fragment, or - // previous activity is not in a task fragment, or - // previous activity's task fragment doesn't adjacent to any others. + if (currTF == prevTF || prevTF.asTask() != null || !prevTF.hasAdjacentTaskFragment()) { + // Current activity and the initPrev is in the same TaskFragment, + // or initPrev activity is a direct child of Task, + // or initPrev activity doesn't have an adjacent. + // A + // B if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) { result.add(initPrev); } return; } - if (prevAdjacentTF == currTF) { + if (currTF.isAdjacentTo(prevTF)) { // previous activity A is adjacent to current activity B. // Try to find anyone below previous activityA, which are C and D if exists. // A | B // C (| D) getActivityBelow(initPrev, inTransition, result); - } else { - // previous activity C isn't adjacent to current activity A. - // A - // B | C - final Task prevAdjacentTask = prevAdjacentTF.getTask(); - if (prevAdjacentTask == currentTask) { - final int currentIndex = currTF != null - ? currentTask.mChildren.indexOf(currTF) - : currentTask.mChildren.indexOf(currentActivity); - final int prevAdjacentIndex = - prevAdjacentTask.mChildren.indexOf(prevAdjacentTF); - // prevAdjacentTF already above currentActivity - if (prevAdjacentIndex > currentIndex) { - return; - } + return; + } + + // The initPrev activity has an adjacent that is different from current activity. + // A + // B | C + final int currentIndex = currTF.asTask() != null + ? currentTask.mChildren.indexOf(currentActivity) + : currentTask.mChildren.indexOf(currTF); + if (!Flags.allowMultipleAdjacentTaskFragments()) { + final int prevAdjacentIndex = currentTask.mChildren.indexOf( + prevTF.getAdjacentTaskFragment()); + if (prevAdjacentIndex > currentIndex) { + // PrevAdjacentTF already above currentActivity + return; } + // Add both the one below, and its adjacent. if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) { result.add(initPrev); } - // prevAdjacentTF is adjacent to another one - final ActivityRecord prevAdjacentActivity = prevAdjacentTF.getTopMostActivity(); + final ActivityRecord prevAdjacentActivity = prevTF.getAdjacentTaskFragment() + .getTopMostActivity(); if (prevAdjacentActivity != null && (!inTransition || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) { result.add(prevAdjacentActivity); } + return; } + + final boolean hasAdjacentAboveCurrent = prevTF.forOtherAdjacentTaskFragments( + prevAdjacentTF -> { + final int prevAdjacentIndex = currentTask.mChildren.indexOf(prevAdjacentTF); + return prevAdjacentIndex > currentIndex; + }); + if (hasAdjacentAboveCurrent) { + // PrevAdjacentTF already above currentActivity + return; + } + // Add all adjacent top. + if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) { + result.add(initPrev); + } + prevTF.forOtherAdjacentTaskFragments(prevAdjacentTF -> { + final ActivityRecord prevAdjacentActivity = prevAdjacentTF.getTopMostActivity(); + if (prevAdjacentActivity != null && (!inTransition + || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) { + result.add(prevAdjacentActivity); + } + }); } static boolean isInParticipant(ActivityRecord ar, diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index 6709e3a72db0..a318c4bf334f 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.Manifest.permission.MANAGE_ACTIVITY_TASKS; import static android.app.ActivityManager.INTENT_SENDER_ACTIVITY; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; @@ -35,6 +36,7 @@ import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME; import static android.content.pm.ApplicationInfo.FLAG_SUSPENDED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; @@ -510,6 +512,14 @@ class ActivityStartInterceptor { } if (mComponentSpecified) { + Slog.w(TAG, "Starting home with component specified, uid=" + mCallingUid); + if (mService.isCallerRecents(mCallingUid) + || ActivityTaskManagerService.checkPermission(MANAGE_ACTIVITY_TASKS, + mCallingPid, mCallingUid) == PERMISSION_GRANTED) { + // Allow home component specified from trusted callers. + return false; + } + final ComponentName homeComponent = mIntent.getComponent(); final Intent homeIntent = mService.getHomeIntent(); final ActivityInfo aInfo = mService.mRootWindowContainer.resolveHomeActivity( diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 0aff1de72cb1..ef6f92317b2c 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2526,9 +2526,6 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { task.forAllActivities(r -> { if (!r.attachedToProcess()) return; mPipModeChangedActivities.add(r); - // If we are scheduling pip change, then remove this activity from multi-window - // change list as the processing of pip change will make sure multi-window changed - // message is processed in the right order relative to pip changed. mMultiWindowModeChangedActivities.remove(r); }); diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index 7deb6a8232be..dbe0faf942d9 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -270,6 +270,11 @@ class BLASTSyncEngine { () -> callback.onCommitted(new SurfaceControl.Transaction())); mHandler.postDelayed(callback, BLAST_TIMEOUT_DURATION); + if (mWm.mAnimator.mPendingState == WindowAnimator.PENDING_STATE_NEED_APPLY) { + // Applies pending transaction before onTransactionReady to ensure the order with + // sync transaction. This is unlikely to happen unless animator thread is slow. + mWm.mAnimator.applyPendingTransaction(); + } Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "onTransactionReady"); mListener.onTransactionReady(mSyncId, merged); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); @@ -353,6 +358,10 @@ class BLASTSyncEngine { + " for non-sync " + wc); wc.mSyncGroup = null; } + if (mWm.mAnimator.mPendingState == WindowAnimator.PENDING_STATE_HAS_CHANGES + && wc.mSyncState != WindowContainer.SYNC_STATE_NONE) { + mWm.mAnimator.mPendingState = WindowAnimator.PENDING_STATE_NEED_APPLY; + } if (mReady) { mWm.mWindowPlacerLocked.requestTraversal(); } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index fc0df645a2db..819395ac60b8 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -453,7 +453,7 @@ class BackNavigationController { outPrevActivities.add(prevActivity); return true; } - if (currTF.getAdjacentTaskFragment() == null) { + if (!currTF.hasAdjacentTaskFragment()) { final TaskFragment nextTF = findNextTaskFragment(currentTask, currTF); if (isSecondCompanionToFirst(currTF, nextTF)) { // TF is isStacked, search bottom activity from companion TF. @@ -476,7 +476,21 @@ class BackNavigationController { } } else { // If adjacent TF has companion to current TF, those two TF will be closed together. - final TaskFragment adjacentTF = currTF.getAdjacentTaskFragment(); + final TaskFragment adjacentTF; + if (Flags.allowMultipleAdjacentTaskFragments()) { + if (currTF.getAdjacentTaskFragments().size() > 2) { + throw new IllegalStateException( + "Not yet support 3+ adjacent for non-Task TFs"); + } + final TaskFragment[] tmpAdjacent = new TaskFragment[1]; + currTF.forOtherAdjacentTaskFragments(tf -> { + tmpAdjacent[0] = tf; + return true; + }); + adjacentTF = tmpAdjacent[0]; + } else { + adjacentTF = currTF.getAdjacentTaskFragment(); + } if (isSecondCompanionToFirst(currTF, adjacentTF)) { // The two TFs are adjacent (visually displayed side-by-side), search if any // activity below the lowest one. @@ -533,29 +547,47 @@ class BackNavigationController { return; } - final TaskFragment prevTFAdjacent = prevTF.getAdjacentTaskFragment(); - if (prevTFAdjacent == null || prevTFAdjacent.asTask() != null) { + if (!prevTF.hasAdjacentTaskFragment()) { return; } - final ActivityRecord prevActivityAdjacent = - prevTFAdjacent.getTopNonFinishingActivity(); - if (prevActivityAdjacent != null) { - outPrevActivities.add(prevActivityAdjacent); + if (!Flags.allowMultipleAdjacentTaskFragments()) { + final TaskFragment prevTFAdjacent = prevTF.getAdjacentTaskFragment(); + final ActivityRecord prevActivityAdjacent = + prevTFAdjacent.getTopNonFinishingActivity(); + if (prevActivityAdjacent != null) { + outPrevActivities.add(prevActivityAdjacent); + } + return; } + prevTF.forOtherAdjacentTaskFragments(prevTFAdjacent -> { + final ActivityRecord prevActivityAdjacent = + prevTFAdjacent.getTopNonFinishingActivity(); + if (prevActivityAdjacent != null) { + outPrevActivities.add(prevActivityAdjacent); + } + }); } private static void findAdjacentActivityIfExist(@NonNull ActivityRecord mainActivity, @NonNull ArrayList<ActivityRecord> outList) { final TaskFragment mainTF = mainActivity.getTaskFragment(); - if (mainTF == null || mainTF.getAdjacentTaskFragment() == null) { + if (mainTF == null || !mainTF.hasAdjacentTaskFragment()) { return; } - final TaskFragment adjacentTF = mainTF.getAdjacentTaskFragment(); - final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity(); - if (topActivity == null) { + if (!Flags.allowMultipleAdjacentTaskFragments()) { + final TaskFragment adjacentTF = mainTF.getAdjacentTaskFragment(); + final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity(); + if (topActivity != null) { + outList.add(topActivity); + } return; } - outList.add(topActivity); + mainTF.forOtherAdjacentTaskFragments(adjacentTF -> { + final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity(); + if (topActivity != null) { + outList.add(topActivity); + } + }); } private static boolean hasTranslucentActivity(@NonNull ActivityRecord currentActivity, diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index f9a06e2dd04b..66b77b9d4d2a 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -90,6 +90,7 @@ import com.android.internal.util.Preconditions; import com.android.server.UiThread; import com.android.server.am.PendingIntentRecord; import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfiguration; +import com.android.window.flags.Flags; import java.lang.annotation.Retention; import java.util.ArrayList; @@ -1650,18 +1651,27 @@ public class BackgroundActivityStartController { return bas; } - TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); - if (adjacentTaskFragment == null) { + if (!taskFragment.hasAdjacentTaskFragment()) { return bas; } - // Check the second fragment. - topActivity = adjacentTaskFragment.getActivity(topOfStackPredicate); - if (topActivity == null) { - return bas; + // Check the adjacent fragment. + if (!Flags.allowMultipleAdjacentTaskFragments()) { + TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); + topActivity = adjacentTaskFragment.getActivity(topOfStackPredicate); + if (topActivity == null) { + return bas; + } + return checkCrossUidActivitySwitchFromBelow(topActivity, uid, bas); } - - return checkCrossUidActivitySwitchFromBelow(topActivity, uid, bas); + final BlockActivityStart[] out = { bas }; + taskFragment.forOtherAdjacentTaskFragments(adjacentTaskFragment -> { + final ActivityRecord top = adjacentTaskFragment.getActivity(topOfStackPredicate); + if (top != null) { + out[0] = checkCrossUidActivitySwitchFromBelow(top, uid, out[0]); + } + }); + return out[0]; } /** diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 5c3fbdfcff0e..09214cd6c553 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -100,6 +100,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_SCREEN_ON; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WALLPAPER; import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_TRANSACTIONS; import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN; +import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_CONFIG; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT; @@ -3288,6 +3289,32 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return new Point(w, h); } + void onDisplayInfoChangeApplied() { + if (!enableDisplayContentModeManagement()) { + Slog.e(TAG, "ShouldShowSystemDecors shouldn't be updated when the flag is off."); + } + + final boolean shouldShow; + if (isDefaultDisplay) { + shouldShow = true; + } else if (isPrivate()) { + shouldShow = false; + } else { + shouldShow = mDisplay.canHostTasks(); + } + + if (shouldShow == mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(this)) { + return; + } + mWmService.mDisplayWindowSettings.setShouldShowSystemDecorsLocked(this, shouldShow); + + if (shouldShow) { + mRootWindowContainer.startSystemDecorations(this, "onDisplayInfoChangeApplied"); + } else { + clearAllTasksOnDisplay(null); + } + } + DisplayCutout loadDisplayCutout(int displayWidth, int displayHeight) { if (mDisplayPolicy == null || mInitialDisplayCutout == null) { return null; @@ -6522,10 +6549,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return mRemoving; } - void remove() { - mRemoving = true; + private void clearAllTasksOnDisplay(@Nullable Runnable clearTasksCallback) { Task lastReparentedRootTask; - mRootWindowContainer.mTaskSupervisor.beginDeferResume(); try { lastReparentedRootTask = reduceOnAllTaskDisplayAreas((taskDisplayArea, rootTask) -> { @@ -6538,10 +6563,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } finally { mRootWindowContainer.mTaskSupervisor.endDeferResume(); } - mRemoved = true; - if (mContentRecorder != null) { - mContentRecorder.stopRecording(); + if (clearTasksCallback != null) { + clearTasksCallback.run(); } // Only update focus/visibility for the last one because there may be many root tasks are @@ -6549,6 +6573,19 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (lastReparentedRootTask != null) { lastReparentedRootTask.resumeNextFocusAfterReparent(); } + } + + void remove() { + mRemoving = true; + + clearAllTasksOnDisplay(() -> { + mRemoved = true; + + if (mContentRecorder != null) { + mContentRecorder.stopRecording(); + } + }); + releaseSelfIfNeeded(); mDisplayPolicy.release(); diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java index 63af5c60b8d3..a017a1173d97 100644 --- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java +++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java @@ -23,6 +23,8 @@ import static com.android.server.wm.Task.TAG_VISIBILITY; import android.annotation.Nullable; import android.util.Slog; +import com.android.window.flags.Flags; + import java.util.ArrayList; /** Helper class to ensure activities are in the right visible state for a container. */ @@ -110,21 +112,37 @@ class EnsureActivitiesVisibleHelper { if (adjacentTaskFragments != null && adjacentTaskFragments.contains( childTaskFragment)) { - if (!childTaskFragment.isTranslucent(starting) - && !childTaskFragment.getAdjacentTaskFragment().isTranslucent( - starting)) { + final boolean isTranslucent; + if (Flags.allowMultipleAdjacentTaskFragments()) { + isTranslucent = childTaskFragment.isTranslucent(starting) + || childTaskFragment.forOtherAdjacentTaskFragments( + adjacentTaskFragment -> { + return adjacentTaskFragment.isTranslucent(starting); + }); + } else { + isTranslucent = childTaskFragment.isTranslucent(starting) + || childTaskFragment.getAdjacentTaskFragment() + .isTranslucent(starting); + } + if (!isTranslucent) { // Everything behind two adjacent TaskFragments are occluded. mBehindFullyOccludedContainer = true; } continue; } - final TaskFragment adjacentTaskFrag = childTaskFragment.getAdjacentTaskFragment(); - if (adjacentTaskFrag != null) { + if (childTaskFragment.hasAdjacentTaskFragment()) { if (adjacentTaskFragments == null) { adjacentTaskFragments = new ArrayList<>(); } - adjacentTaskFragments.add(adjacentTaskFrag); + if (Flags.allowMultipleAdjacentTaskFragments()) { + final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments; + childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { + adjacentTfs.add(adjacentTf); + }); + } else { + adjacentTaskFragments.add(childTaskFragment.getAdjacentTaskFragment()); + } } } else if (child.asActivityRecord() != null) { setActivityVisibilityState(child.asActivityRecord(), starting, resumeTopActivity); diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java index 06049530da18..790858d2eec2 100644 --- a/services/core/java/com/android/server/wm/LockTaskController.java +++ b/services/core/java/com/android/server/wm/LockTaskController.java @@ -263,10 +263,9 @@ public class LockTaskController { // should be finish together in the Task. if (activity != taskRoot || activity != taskTop) { final TaskFragment taskFragment = activity.getTaskFragment(); - final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); if (taskFragment.asTask() != null || !taskFragment.isDelayLastActivityRemoval() - || adjacentTaskFragment == null) { + || !taskFragment.hasAdjacentTaskFragment()) { // Don't block activity from finishing if the TaskFragment don't have any adjacent // TaskFragment, or it won't finish together with its adjacent TaskFragment. return false; @@ -281,7 +280,7 @@ public class LockTaskController { } final boolean hasOtherActivityInTask = task.getActivity(a -> !a.finishing - && a != activity && a.getTaskFragment() != adjacentTaskFragment) != null; + && a != activity && !taskFragment.isAdjacentTo(a.getTaskFragment())) != null; if (hasOtherActivityInTask) { // Do not block activity from finishing if there are another running activities // after the current and adjacent TaskFragments are removed. Note that we don't @@ -653,6 +652,10 @@ public class LockTaskController { if (!isSystemCaller) { task.mLockTaskUid = callingUid; if (task.mLockTaskAuth == LOCK_TASK_AUTH_PINNABLE) { + if (mLockTaskModeTasks.contains(task)) { + ProtoLog.w(WM_DEBUG_LOCKTASK, "Already locked."); + return; + } // startLockTask() called by app, but app is not part of lock task allowlist. Show // app pinning request. We will come back here with isSystemCaller true. ProtoLog.w(WM_DEBUG_LOCKTASK, "Mode default, asking user"); diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 4f36476c674f..57fe0bb4937e 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -46,6 +46,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_STATES; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TASKS; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WALLPAPER; import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_SURFACE_ALLOC; +import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; import static com.android.server.policy.PhoneWindowManager.SYSTEM_DIALOG_REASON_ASSIST; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; @@ -151,6 +152,7 @@ import com.android.server.LocalServices; import com.android.server.am.ActivityManagerService; import com.android.server.am.AppTimeTracker; import com.android.server.am.UserState; +import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.PermissionPolicyInternal; import com.android.server.policy.WindowManagerPolicy; @@ -1438,6 +1440,13 @@ class RootWindowContainer extends WindowContainer<DisplayContent> : getDefaultTaskDisplayArea(); } + // When display content mode management flag is enabled, the task display area is marked as + // removed when switching from extended display to mirroring display. We need to restart the + // task display area before starting the home. + if (enableDisplayContentModeManagement() && taskDisplayArea.isRemoved()) { + taskDisplayArea.restart(); + } + Intent homeIntent = null; ActivityInfo aInfo = null; if (taskDisplayArea == getDefaultTaskDisplayArea() @@ -2856,20 +2865,24 @@ class RootWindowContainer extends WindowContainer<DisplayContent> if (display == null) { return; } - // Do not start home before booting, or it may accidentally finish booting before it - // starts. Instead, we expect home activities to be launched when the system is ready - // (ActivityManagerService#systemReady). - if (mService.isBooted() || mService.isBooting()) { - startSystemDecorations(display); - } + + startSystemDecorations(display, "displayAdded"); + // Drop any cached DisplayInfos associated with this display id - the values are now // out of date given this display added event. mWmService.mPossibleDisplayInfoMapper.removePossibleDisplayInfos(displayId); } } - private void startSystemDecorations(final DisplayContent displayContent) { - startHomeOnDisplay(mCurrentUser, "displayAdded", displayContent.getDisplayId()); + void startSystemDecorations(final DisplayContent displayContent, String reason) { + // Do not start home before booting, or it may accidentally finish booting before it + // starts. Instead, we expect home activities to be launched when the system is ready + // (ActivityManagerService#systemReady). + if (!mService.isBooted() && !mService.isBooting()) { + return; + } + + startHomeOnDisplay(mCurrentUser, reason, displayContent.getDisplayId()); displayContent.getDisplayPolicy().notifyDisplayReady(); } @@ -2896,7 +2909,13 @@ class RootWindowContainer extends WindowContainer<DisplayContent> synchronized (mService.mGlobalLock) { final DisplayContent displayContent = getDisplayContent(displayId); if (displayContent != null) { - displayContent.requestDisplayUpdate(() -> clearDisplayInfoCaches(displayId)); + displayContent.requestDisplayUpdate( + () -> { + clearDisplayInfoCaches(displayId); + if (enableDisplayContentModeManagement()) { + displayContent.onDisplayInfoChangeApplied(); + } + }); } else { clearDisplayInfoCaches(displayId); } diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java index 9a48d5b8880d..d7b6d96c781d 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimator.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java @@ -200,6 +200,7 @@ public class SurfaceAnimator { } mSnapshot.startAnimation(t, snapshotAnim, type); } + setAnimatorPendingState(t); } void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden, @@ -208,6 +209,14 @@ public class SurfaceAnimator { null /* animationCancelledCallback */, null /* snapshotAnim */, null /* freezer */); } + /** Indicates that there are surface operations in the pending transaction. */ + private void setAnimatorPendingState(Transaction t) { + if (mService.mAnimator.mPendingState == WindowAnimator.PENDING_STATE_NONE + && t == mAnimatable.getPendingTransaction()) { + mService.mAnimator.mPendingState = WindowAnimator.PENDING_STATE_HAS_CHANGES; + } + } + /** Returns whether it is currently running an animation. */ boolean isAnimating() { return mAnimation != null; @@ -357,6 +366,7 @@ public class SurfaceAnimator { final boolean scheduleAnim = removeLeash(t, mAnimatable, leash, destroyLeash); mAnimationFinished = false; if (scheduleAnim) { + setAnimatorPendingState(t); mService.scheduleAnimationLocked(); } } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 3d0b41ba3a0f..3634bc987d0a 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -1887,6 +1887,11 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { return lastReparentedRootTask; } + // TODO(b/385263090): Remove this method + void restart() { + mRemoved = false; + } + /** * Returns the {@link TaskDisplayArea} to which root tasks should be reparented. * diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index cb6b69072e14..367adc355738 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -518,11 +518,14 @@ class TaskFragment extends WindowContainer<WindowContainer> { } } - // TODO(b/373709676): update usages. /** @deprecated b/373709676 replace with {@link #getAdjacentTaskFragments()}. */ @Deprecated @Nullable TaskFragment getAdjacentTaskFragment() { + if (Flags.allowMultipleAdjacentTaskFragments()) { + throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. " + + "Use #getAdjacentTaskFragments instead"); + } return mAdjacentTaskFragment; } @@ -3523,10 +3526,18 @@ class TaskFragment extends WindowContainer<WindowContainer> { throw new IllegalStateException("allowMultipleAdjacentTaskFragments must be" + " enabled to set more than two TaskFragments adjacent to each other."); } - if (taskFragments.size() < 2) { + final int size = taskFragments.size(); + if (size < 2) { throw new IllegalArgumentException("Adjacent TaskFragments must contain at least" - + " two TaskFragments, but only " + taskFragments.size() - + " were provided."); + + " two TaskFragments, but only " + size + " were provided."); + } + if (size > 2) { + for (int i = 0; i < size; i++) { + if (taskFragments.valueAt(i).asTask() == null) { + throw new IllegalArgumentException( + "Not yet support 3+ adjacent for non-Task TFs"); + } + } } mAdjacentSet = taskFragments; } @@ -3604,6 +3615,10 @@ class TaskFragment extends WindowContainer<WindowContainer> { return false; } + int size() { + return mAdjacentSet.size(); + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java index 49c8559c02a8..790ae1eef0c3 100644 --- a/services/core/java/com/android/server/wm/WindowAnimator.java +++ b/services/core/java/com/android/server/wm/WindowAnimator.java @@ -26,6 +26,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_TRACE; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; +import android.annotation.IntDef; import android.content.Context; import android.os.HandlerExecutor; import android.os.Trace; @@ -38,6 +39,8 @@ import com.android.internal.protolog.ProtoLog; import com.android.server.policy.WindowManagerPolicy; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; /** @@ -86,6 +89,25 @@ public class WindowAnimator { private final SurfaceControl.Transaction mTransaction; + /** The pending transaction is applied. */ + static final int PENDING_STATE_NONE = 0; + /** There are some (significant) operations set to the pending transaction. */ + static final int PENDING_STATE_HAS_CHANGES = 1; + /** The pending transaction needs to be applied before sending sync transaction to shell. */ + static final int PENDING_STATE_NEED_APPLY = 2; + + @IntDef(prefix = { "PENDING_STATE_" }, value = { + PENDING_STATE_NONE, + PENDING_STATE_HAS_CHANGES, + PENDING_STATE_NEED_APPLY, + }) + @Retention(RetentionPolicy.SOURCE) + @interface PendingState {} + + /** The global state of pending transaction. */ + @PendingState + int mPendingState; + WindowAnimator(final WindowManagerService service) { mService = service; mContext = service.mContext; @@ -217,6 +239,7 @@ public class WindowAnimator { Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "applyTransaction"); mTransaction.apply(); Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + mPendingState = PENDING_STATE_NONE; mService.mWindowTracing.logState("WindowAnimator"); ProtoLog.i(WM_SHOW_TRANSACTIONS, "<<< CLOSE TRANSACTION animate"); @@ -296,8 +319,19 @@ public class WindowAnimator { return mAnimationFrameCallbackScheduled; } - Choreographer getChoreographer() { - return mChoreographer; + void applyPendingTransaction() { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "applyPendingTransaction"); + mPendingState = PENDING_STATE_NONE; + final int numDisplays = mService.mRoot.getChildCount(); + if (numDisplays == 1) { + mService.mRoot.getChildAt(0).getPendingTransaction().apply(); + } else { + for (int i = 0; i < numDisplays; i++) { + mTransaction.merge(mService.mRoot.getChildAt(i).getPendingTransaction()); + } + mTransaction.apply(); + } + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 793f18992109..965b22473a2c 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -9150,7 +9150,7 @@ public class WindowManagerService extends IWindowManager.Stub // handling the touch-outside event to prevent focus rapid changes back-n-forth. final boolean shouldDelayTouchForEmbeddedActivity = activity != null && activity.isEmbedded() - && activity.getTaskFragment().getAdjacentTaskFragment() != null; + && activity.getTaskFragment().hasAdjacentTaskFragment(); // For cases when there are multiple freeform windows where non-top windows are blocking // the gesture zones, delay handling the touch-outside event to prevent refocusing the @@ -9529,21 +9529,41 @@ public class WindowManagerService extends IWindowManager.Stub return focusedActivity; } - final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); - final ActivityRecord adjacentTopActivity = - adjacentTaskFragment != null ? adjacentTaskFragment.topRunningActivity() : null; - if (adjacentTopActivity == null) { - // Return if no adjacent activity. + if (!taskFragment.hasAdjacentTaskFragment()) { return focusedActivity; } - if (adjacentTopActivity.getLastWindowCreateTime() - < focusedActivity.getLastWindowCreateTime()) { - // Return if the current focus activity has more recently active window. - return focusedActivity; + if (!Flags.allowMultipleAdjacentTaskFragments()) { + final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); + final ActivityRecord adjacentTopActivity = adjacentTaskFragment.topRunningActivity(); + if (adjacentTopActivity == null) { + // Return if no adjacent activity. + return focusedActivity; + } + + if (adjacentTopActivity.getLastWindowCreateTime() + < focusedActivity.getLastWindowCreateTime()) { + // Return if the current focus activity has more recently active window. + return focusedActivity; + } + + return adjacentTopActivity; } - return adjacentTopActivity; + // Find the adjacent activity with more recently active window. + final ActivityRecord[] mostRecentActiveActivity = { focusedActivity }; + final long[] mostRecentActiveTime = { focusedActivity.getLastWindowCreateTime() }; + taskFragment.forOtherAdjacentTaskFragments(adjacentTaskFragment -> { + final ActivityRecord adjacentTopActivity = adjacentTaskFragment.topRunningActivity(); + if (adjacentTopActivity != null) { + final long lastWindowCreateTime = adjacentTopActivity.getLastWindowCreateTime(); + if (lastWindowCreateTime > mostRecentActiveTime[0]) { + mostRecentActiveTime[0] = lastWindowCreateTime; + mostRecentActiveActivity[0] = adjacentTopActivity; + } + } + }); + return mostRecentActiveActivity[0]; } @NonNull @@ -9592,14 +9612,28 @@ public class WindowManagerService extends IWindowManager.Stub return false; } final TaskFragment fromFragment = fromWin.getTaskFragment(); - if (fromFragment == null) { + if (fromFragment == null || fromFragment.asTask() != null) { + // Don't move the focus to another task. return false; } - final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment(); - if (adjacentFragment == null || adjacentFragment.asTask() != null) { - // Don't move the focus to another task. + if (!fromFragment.hasAdjacentTaskFragment()) { + // No adjacent window. return false; } + final TaskFragment adjacentFragment; + if (Flags.allowMultipleAdjacentTaskFragments()) { + if (fromFragment.getAdjacentTaskFragments().size() > 2) { + throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs"); + } + final TaskFragment[] tmpAdjacent = new TaskFragment[1]; + fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> { + tmpAdjacent[0] = adjacentTF; + return true; + }); + adjacentFragment = tmpAdjacent[0]; + } else { + adjacentFragment = fromFragment.getAdjacentTaskFragment(); + } if (adjacentFragment.isIsolatedNav()) { // Don't move the focus if the adjacent TF is isolated navigation. return false; diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index fb197c566b7d..e45ada9438ae 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -80,6 +80,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_ORG import static com.android.server.wm.ActivityRecord.State.PAUSING; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission; +import static com.android.server.wm.ActivityTaskManagerService.isPip2ExperimentEnabled; import static com.android.server.wm.ActivityTaskSupervisor.REMOVE_FROM_RECENTS; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; @@ -716,6 +717,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } if (forceHiddenForPip) { wc.asTask().setForceHidden(FLAG_FORCE_HIDDEN_FOR_PINNED_TASK, true /* set */); + } + if (forceHiddenForPip && !isPip2ExperimentEnabled()) { // When removing pip, make sure that onStop is sent to the app ahead of // onPictureInPictureModeChanged. // See also PinnedStackTests#testStopBeforeMultiWindowCallbacksOnDismiss diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java index 0ae7699aeb71..58e4b9177808 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java @@ -36,6 +36,7 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.content.Context; import android.content.Intent; +import android.os.Binder; import android.os.Handler; import android.os.Message; import android.os.UserHandle; @@ -233,6 +234,7 @@ public class BroadcastHelperTest { mBroadcastHelper.sendPackageChangedBroadcast(mMockSnapshot, PACKAGE_CHANGED_TEST_PACKAGE_NAME, true /* dontKillApp */, componentNames, - UserHandle.USER_SYSTEM, "test" /* reason */, "test" /* reasonForTrace */); + UserHandle.USER_SYSTEM, "test" /* reason */, "test" /* reasonForTrace */, + Binder.getCallingUid()); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java index 76f7e80a3412..0972ea91ea73 100644 --- a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java @@ -33,7 +33,6 @@ import android.content.Context; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import android.util.LongSparseArray; @@ -72,6 +71,7 @@ public class BlobStoreManagerServiceTest { private static final String TEST_PKG2 = "com.example2"; private static final String TEST_PKG3 = "com.example3"; + private static final int TEST_USER_ID = 0; private static final int TEST_UID1 = 10001; private static final int TEST_UID2 = 10002; private static final int TEST_UID3 = 10003; @@ -98,7 +98,7 @@ public class BlobStoreManagerServiceTest { mService = new BlobStoreManagerService(mContext, new TestInjector()); mUserSessions = new LongSparseArray<>(); - mService.addUserSessionsForTest(mUserSessions, UserHandle.myUserId()); + mService.addUserSessionsForTest(mUserSessions, TEST_USER_ID); } @After @@ -360,6 +360,7 @@ public class BlobStoreManagerServiceTest { return createBlobStoreSessionMock(ownerPackageName, ownerUid, sessionId, sessionFile, mock(BlobHandle.class)); } + private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid, long sessionId, File sessionFile, BlobHandle blobHandle) { final BlobStoreSession session = mock(BlobStoreSession.class); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 94a40020cfc8..e3e9cc426bb3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -361,12 +361,6 @@ public class ActivityStarterTests extends WindowTestsBase { return prepareStarter(launchFlags, mockGetRootTask, LAUNCH_MULTIPLE); } - private void setupImeWindow() { - final WindowState imeWindow = createWindow(null, W_INPUT_METHOD, - "mImeWindow", CURRENT_IME_UID); - mDisplayContent.mInputMethodWindow = imeWindow; - } - /** * Creates a {@link ActivityStarter} with default parameters and necessary mocks. * diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 57aacd36b16b..5cd2a994d81a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -85,6 +85,7 @@ import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE; +import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT; import static com.google.common.truth.Truth.assertThat; @@ -2879,6 +2880,43 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()); } + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testSetShouldShowSystemDecorations_defaultDisplay() { + DisplayContent dc = mWm.mRoot.getDisplayContent(DEFAULT_DISPLAY); + + dc.onDisplayInfoChangeApplied(); + assertTrue(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); + } + + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testSetShouldShowSystemDecorations_privateDisplay() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.flags = FLAG_PRIVATE; + final DisplayContent dc = createNewDisplay(displayInfo); + + dc.onDisplayInfoChangeApplied(); + assertFalse(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); + } + + @EnableFlags(FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT) + @Test + public void testSetShouldShowSystemDecorations_nonDefaultNonPrivateDisplay() { + final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + final DisplayContent dc = createNewDisplay(displayInfo); + + spyOn(dc.mDisplay); + doReturn(false).when(dc.mDisplay).canHostTasks(); + dc.onDisplayInfoChangeApplied(); + assertFalse(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); + + doReturn(true).when(dc.mDisplay).canHostTasks(); + dc.onDisplayInfoChangeApplied(); + assertTrue(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); + } + private void removeRootTaskTests(Runnable runnable) { final TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea(); final Task rootTask1 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN, diff --git a/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java b/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java index c016c5ead23c..de0716885214 100644 --- a/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java @@ -73,7 +73,7 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { private static final float MID_REFRESH_RATE = 70; private static final float LOW_REFRESH_RATE = 60; WindowState createWindow(String name) { - WindowState window = createWindow(null, TYPE_APPLICATION, name); + WindowState window = newWindowBuilder(name, TYPE_APPLICATION).build(); when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) .thenReturn(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS); return window; diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java index f70dcebce30d..7d59f4872d37 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java @@ -56,12 +56,13 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { @Test public void testTransparentControlTargetWindowCanShowIme() { - final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build(); makeWindowVisibleAndDrawn(ime); mImeProvider.setWindowContainer(ime, null, null); - final WindowState appWin = createWindow(null, TYPE_APPLICATION, "app"); - final WindowState popup = createWindow(appWin, TYPE_APPLICATION, "popup"); + final WindowState appWin = newWindowBuilder("app", TYPE_APPLICATION).build(); + final WindowState popup = newWindowBuilder("popup", TYPE_APPLICATION).setParent( + appWin).build(); popup.mAttrs.format = PixelFormat.TRANSPARENT; mDisplayContent.setImeLayeringTarget(appWin); mDisplayContent.updateImeInputAndControlTarget(popup); @@ -77,11 +78,11 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { @Test @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testScheduleShowIme() { - final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build(); makeWindowVisibleAndDrawn(ime); mImeProvider.setWindowContainer(ime, null, null); - final WindowState target = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState target = newWindowBuilder("app", TYPE_APPLICATION).build(); mDisplayContent.setImeLayeringTarget(target); mDisplayContent.updateImeInputAndControlTarget(target); performSurfacePlacementAndWaitForWindowAnimator(); @@ -105,14 +106,14 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { @Test @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testScheduleShowIme_noInitialState() { - final WindowState target = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState target = newWindowBuilder("app", TYPE_APPLICATION).build(); // Schedule before anything is ready. mImeProvider.scheduleShowImePostLayout(target, ImeTracker.Token.empty()); assertFalse(mImeProvider.isScheduledAndReadyToShowIme()); assertFalse(mImeProvider.isImeShowing()); - final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build(); makeWindowVisibleAndDrawn(ime); mImeProvider.setWindowContainer(ime, null, null); @@ -133,11 +134,11 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { @Test @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testScheduleShowIme_delayedAfterPrepareSurfaces() { - final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build(); makeWindowVisibleAndDrawn(ime); mImeProvider.setWindowContainer(ime, null, null); - final WindowState target = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState target = newWindowBuilder("app", TYPE_APPLICATION).build(); mDisplayContent.setImeLayeringTarget(target); mDisplayContent.updateImeInputAndControlTarget(target); @@ -166,11 +167,11 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { @Test @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testScheduleShowIme_delayedSurfacePlacement() { - final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build(); makeWindowVisibleAndDrawn(ime); mImeProvider.setWindowContainer(ime, null, null); - final WindowState target = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState target = newWindowBuilder("app", TYPE_APPLICATION).build(); mDisplayContent.setImeLayeringTarget(target); mDisplayContent.updateImeInputAndControlTarget(target); @@ -191,7 +192,7 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { @Test public void testSetFrozen() { - final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build(); makeWindowVisibleAndDrawn(ime); mImeProvider.setWindowContainer(ime, null, null); mImeProvider.setServerVisible(true); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index ee56210e278d..6c5fe1d8551e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -106,8 +106,9 @@ public class InsetsPolicyTest extends WindowTestsBase { addStatusBar(); addNavigationBar(); - final WindowState win = createWindow(null, WINDOWING_MODE_FREEFORM, - ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app"); + final WindowState win = newWindowBuilder("app", TYPE_APPLICATION).setActivityType( + ACTIVITY_TYPE_STANDARD).setWindowingMode(WINDOWING_MODE_FREEFORM).setDisplay( + mDisplayContent).build(); final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win); // The app must not control any system bars. @@ -120,8 +121,9 @@ public class InsetsPolicyTest extends WindowTestsBase { addStatusBar(); addNavigationBar(); - final WindowState win = createWindow(null, WINDOWING_MODE_FREEFORM, - ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app"); + final WindowState win = newWindowBuilder("app", TYPE_APPLICATION).setActivityType( + ACTIVITY_TYPE_STANDARD).setWindowingMode(WINDOWING_MODE_FREEFORM).setDisplay( + mDisplayContent).build(); win.setBounds(new Rect()); final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win); @@ -136,8 +138,9 @@ public class InsetsPolicyTest extends WindowTestsBase { addStatusBar(); addNavigationBar(); - final WindowState win = createWindow(null, WINDOWING_MODE_FREEFORM, - ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app"); + final WindowState win = newWindowBuilder("app", TYPE_APPLICATION).setActivityType( + ACTIVITY_TYPE_STANDARD).setWindowingMode(WINDOWING_MODE_FREEFORM).setDisplay( + mDisplayContent).build(); win.getTask().setBounds(new Rect(1, 1, 10, 10)); final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win); @@ -582,7 +585,7 @@ public class InsetsPolicyTest extends WindowTestsBase { private WindowState addNavigationBar() { final Binder owner = new Binder(); - final WindowState win = createWindow(null, TYPE_NAVIGATION_BAR, "navBar"); + final WindowState win = newWindowBuilder("navBar", TYPE_NAVIGATION_BAR).build(); win.mAttrs.flags |= FLAG_NOT_FOCUSABLE; win.mAttrs.providedInsets = new InsetsFrameProvider[] { new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()), @@ -595,7 +598,7 @@ public class InsetsPolicyTest extends WindowTestsBase { private WindowState addStatusBar() { final Binder owner = new Binder(); - final WindowState win = createWindow(null, TYPE_STATUS_BAR, "statusBar"); + final WindowState win = newWindowBuilder("statusBar", TYPE_STATUS_BAR).build(); win.mAttrs.flags |= FLAG_NOT_FOCUSABLE; win.mAttrs.providedInsets = new InsetsFrameProvider[] { new InsetsFrameProvider(owner, 0, WindowInsets.Type.statusBars()), @@ -607,7 +610,7 @@ public class InsetsPolicyTest extends WindowTestsBase { } private WindowState addWindow(int type, String name) { - final WindowState win = createWindow(null, type, name); + final WindowState win = newWindowBuilder(name, type).build(); mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs); return win; } diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java index 79967b861ea5..c30aa52b1ef4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java @@ -63,7 +63,7 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testPostLayout() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); statusBar.setBounds(0, 0, 500, 1000); statusBar.getFrame().set(0, 0, 500, 100); statusBar.mHasSurface = true; @@ -81,7 +81,7 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testPostLayout_invisible() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); statusBar.setBounds(0, 0, 500, 1000); statusBar.getFrame().set(0, 0, 500, 100); mProvider.setWindowContainer(statusBar, null, null); @@ -93,7 +93,7 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testPostLayout_frameProvider() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); statusBar.getFrame().set(0, 0, 500, 100); statusBar.mHasSurface = true; mProvider.setWindowContainer(statusBar, @@ -108,8 +108,8 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testUpdateControlForTarget() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState target = createWindow(null, TYPE_APPLICATION, "target"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState target = newWindowBuilder("target", TYPE_APPLICATION).build(); statusBar.getFrame().set(0, 0, 500, 100); // We must not have control or control target before we have the insets source window. @@ -153,8 +153,8 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testUpdateControlForFakeTarget() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState target = createWindow(null, TYPE_APPLICATION, "target"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState target = newWindowBuilder("target", TYPE_APPLICATION).build(); statusBar.getFrame().set(0, 0, 500, 100); mProvider.setWindowContainer(statusBar, null, null); mProvider.updateFakeControlTarget(target); @@ -166,10 +166,10 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testGetLeash() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState target = createWindow(null, TYPE_APPLICATION, "target"); - final WindowState fakeTarget = createWindow(null, TYPE_APPLICATION, "fakeTarget"); - final WindowState otherTarget = createWindow(null, TYPE_APPLICATION, "otherTarget"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState target = newWindowBuilder("target", TYPE_APPLICATION).build(); + final WindowState fakeTarget = newWindowBuilder("fakeTarget", TYPE_APPLICATION).build(); + final WindowState otherTarget = newWindowBuilder("otherTarget", TYPE_APPLICATION).build(); statusBar.getFrame().set(0, 0, 500, 100); // We must not have control or control target before we have the insets source window, @@ -208,7 +208,7 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testUpdateSourceFrame() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); mProvider.setWindowContainer(statusBar, null, null); statusBar.setBounds(0, 0, 500, 1000); @@ -238,7 +238,7 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testUpdateSourceFrameForIme() { - final WindowState inputMethod = createWindow(null, TYPE_INPUT_METHOD, "inputMethod"); + final WindowState inputMethod = newWindowBuilder("inputMethod", TYPE_INPUT_METHOD).build(); inputMethod.getFrame().set(new Rect(0, 400, 500, 500)); @@ -262,9 +262,9 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testUpdateInsetsControlPosition() { - final WindowState target = createWindow(null, TYPE_APPLICATION, "target"); + final WindowState target = newWindowBuilder("target", TYPE_APPLICATION).build(); - final WindowState ime1 = createWindow(null, TYPE_INPUT_METHOD, "ime1"); + final WindowState ime1 = newWindowBuilder("ime1", TYPE_INPUT_METHOD).build(); ime1.getFrame().set(new Rect(0, 0, 0, 0)); mImeProvider.setWindowContainer(ime1, null, null); mImeProvider.updateControlForTarget(target, false /* force */, null /* statsToken */); @@ -272,7 +272,7 @@ public class InsetsSourceProviderTest extends WindowTestsBase { mImeProvider.updateInsetsControlPosition(ime1); assertEquals(new Point(0, 400), mImeProvider.getControl(target).getSurfacePosition()); - final WindowState ime2 = createWindow(null, TYPE_INPUT_METHOD, "ime2"); + final WindowState ime2 = newWindowBuilder("ime2", TYPE_INPUT_METHOD).build(); ime2.getFrame().set(new Rect(0, 0, 0, 0)); mImeProvider.setWindowContainer(ime2, null, null); mImeProvider.updateControlForTarget(target, false /* force */, null /* statsToken */); @@ -283,8 +283,8 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testSetRequestedVisibleTypes() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState target = createWindow(null, TYPE_APPLICATION, "target"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState target = newWindowBuilder("target", TYPE_APPLICATION).build(); statusBar.getFrame().set(0, 0, 500, 100); mProvider.setWindowContainer(statusBar, null, null); mProvider.updateControlForTarget(target, false /* force */, null /* statsToken */); @@ -295,8 +295,8 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testSetRequestedVisibleTypes_noControl() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState target = createWindow(null, TYPE_APPLICATION, "target"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState target = newWindowBuilder("target", TYPE_APPLICATION).build(); statusBar.getFrame().set(0, 0, 500, 100); mProvider.setWindowContainer(statusBar, null, null); target.setRequestedVisibleTypes(0, statusBars()); @@ -306,7 +306,7 @@ public class InsetsSourceProviderTest extends WindowTestsBase { @Test public void testInsetGeometries() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); statusBar.getFrame().set(0, 0, 500, 100); statusBar.mHasSurface = true; mProvider.setWindowContainer(statusBar, null, null); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java index 66a66a1e358b..973c8d0a8464 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java @@ -76,9 +76,9 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testStripForDispatch_navBar() { - final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar"); - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState ime = createWindow(null, TYPE_APPLICATION, "ime"); + final WindowState navBar = newWindowBuilder("navBar", TYPE_APPLICATION).build(); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState ime = newWindowBuilder("ime", TYPE_APPLICATION).build(); // IME cannot be the IME target. ime.mAttrs.flags |= FLAG_NOT_FOCUSABLE; @@ -96,9 +96,9 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testStripForDispatch_pip() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar"); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState navBar = newWindowBuilder("navBar", TYPE_APPLICATION).build(); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars()) .setWindowContainer(statusBar, null, null); @@ -113,9 +113,9 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testStripForDispatch_freeform() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar"); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState navBar = newWindowBuilder("navBar", TYPE_APPLICATION).build(); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars()) .setWindowContainer(statusBar, null, null); @@ -129,9 +129,9 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testStripForDispatch_multiwindow_alwaysOnTop() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar"); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState navBar = newWindowBuilder("navBar", TYPE_APPLICATION).build(); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars()) .setWindowContainer(statusBar, null, null); @@ -150,8 +150,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().getOrCreateSourceProvider(ID_IME, ime()) .setWindowContainer(mImeWindow, null, null); - final WindowState app1 = createWindow(null, TYPE_APPLICATION, "app1"); - final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2"); + final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).build(); + final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build(); app1.mAboveInsetsState.addSource(getController().getRawInsetsState().peekSource(ID_IME)); @@ -166,7 +166,7 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().getOrCreateSourceProvider(ID_IME, ime()) .setWindowContainer(mImeWindow, null, null); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); app.mAboveInsetsState.getOrCreateSource(ID_IME, ime()) .setVisible(true) .setFrame(mImeWindow.getFrame()); @@ -181,7 +181,7 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().getOrCreateSourceProvider(ID_IME, ime()) .setWindowContainer(mImeWindow, null, null); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); getController().getRawInsetsState().setSourceVisible(ID_IME, true); assertFalse(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); @@ -193,7 +193,7 @@ public class InsetsStateControllerTest extends WindowTestsBase { // This can be the IME z-order target while app cannot be the IME z-order target. // This is also the only IME control target in this test, so IME won't be invisible caused // by the control-target change. - final WindowState base = createWindow(null, TYPE_APPLICATION, "base"); + final WindowState base = newWindowBuilder("base", TYPE_APPLICATION).build(); mDisplayContent.updateImeInputAndControlTarget(base); // Make IME and stay visible during the test. @@ -210,7 +210,7 @@ public class InsetsStateControllerTest extends WindowTestsBase { } // Send our spy window (app) into the system so that we can detect the invocation. - final WindowState win = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState win = newWindowBuilder("app", TYPE_APPLICATION).build(); win.setHasSurface(true); final WindowToken parent = win.mToken; parent.removeChild(win); @@ -250,8 +250,9 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().getOrCreateSourceProvider(ID_IME, ime()) .setWindowContainer(mImeWindow, null, null); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); - final WindowState child = createWindow(app, TYPE_APPLICATION, "child"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); + final WindowState child = newWindowBuilder("child", TYPE_APPLICATION).setParent( + app).build(); app.mAboveInsetsState.set(getController().getRawInsetsState()); child.mAboveInsetsState.set(getController().getRawInsetsState()); child.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; @@ -271,8 +272,9 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().getOrCreateSourceProvider(ID_IME, ime()) .setWindowContainer(mImeWindow, null, null); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); - final WindowState child = createWindow(app, TYPE_APPLICATION, "child"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); + final WindowState child = newWindowBuilder("child", TYPE_APPLICATION).setParent( + app).build(); app.mAboveInsetsState.addSource(getController().getRawInsetsState().peekSource(ID_IME)); child.mAttrs.flags |= FLAG_NOT_FOCUSABLE; child.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); @@ -288,8 +290,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testImeForDispatch() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build(); makeWindowVisible(statusBar); @@ -318,11 +320,11 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testBarControllingWinChanged() { - final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar"); - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState climateBar = createWindow(null, TYPE_APPLICATION, "climateBar"); - final WindowState extraNavBar = createWindow(null, TYPE_APPLICATION, "extraNavBar"); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState navBar = newWindowBuilder("navBar", TYPE_APPLICATION).build(); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState climateBar = newWindowBuilder("climateBar", TYPE_APPLICATION).build(); + final WindowState extraNavBar = newWindowBuilder("extraNavBar", TYPE_APPLICATION).build(); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars()) .setWindowContainer(statusBar, null, null); getController().getOrCreateSourceProvider(ID_NAVIGATION_BAR, navigationBars()) @@ -338,8 +340,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testControlRevoked() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars()) .setWindowContainer(statusBar, null, null); getController().onBarControlTargetChanged(app, null, null, null); @@ -350,8 +352,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testControlRevoked_animation() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars()) .setWindowContainer(statusBar, null, null); getController().onBarControlTargetChanged(app, null, null, null); @@ -362,19 +364,20 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testControlTargetChangedWhileProviderHasNoWindow() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); final InsetsSourceProvider provider = getController().getOrCreateSourceProvider( ID_STATUS_BAR, statusBars()); getController().onBarControlTargetChanged(app, null, null, null); assertNull(getController().getControlsForDispatch(app)); - provider.setWindowContainer(createWindow(null, TYPE_APPLICATION, "statusBar"), null, null); + provider.setWindowContainer(newWindowBuilder("statusBar", TYPE_APPLICATION).build(), null, + null); assertNotNull(getController().getControlsForDispatch(app)); } @Test public void testTransientVisibilityOfFixedRotationState() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); final InsetsSourceProvider provider = getController() .getOrCreateSourceProvider(ID_STATUS_BAR, statusBars()); provider.setWindowContainer(statusBar, null, null); @@ -494,7 +497,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testUpdateAboveInsetsState_imeTargetOnScreenBehavior() { final WindowToken imeToken = createTestWindowToken(TYPE_INPUT_METHOD, mDisplayContent); - final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, imeToken, "ime"); + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).setWindowToken( + imeToken).build(); final WindowState app = createTestWindow("app"); getController().getOrCreateSourceProvider(ID_IME, ime()) @@ -538,10 +542,10 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testDispatchGlobalInsets() { - final WindowState navBar = createWindow(null, TYPE_APPLICATION, "navBar"); + final WindowState navBar = newWindowBuilder("navBar", TYPE_APPLICATION).build(); getController().getOrCreateSourceProvider(ID_NAVIGATION_BAR, navigationBars()) .setWindowContainer(navBar, null, null); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); assertNull(app.getInsetsState().peekSource(ID_NAVIGATION_BAR)); app.mAttrs.receiveInsetsIgnoringZOrder = true; assertNotNull(app.getInsetsState().peekSource(ID_NAVIGATION_BAR)); @@ -580,8 +584,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { @Test public void testHasPendingControls() { - final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_APPLICATION).build(); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); getController().getOrCreateSourceProvider(ID_STATUS_BAR, statusBars()) .setWindowContainer(statusBar, null, null); // No controls dispatched yet. @@ -598,7 +602,7 @@ public class InsetsStateControllerTest extends WindowTestsBase { /** Creates a window which is associated with ActivityRecord. */ private WindowState createTestWindow(String name) { - final WindowState win = createWindow(null, TYPE_APPLICATION, name); + final WindowState win = newWindowBuilder(name, TYPE_APPLICATION).build(); win.setHasSurface(true); spyOn(win); return win; @@ -606,7 +610,7 @@ public class InsetsStateControllerTest extends WindowTestsBase { /** Creates a non-activity window. */ private WindowState createNonAppWindow(String name) { - final WindowState win = createWindow(null, LAST_APPLICATION_WINDOW + 1, name); + final WindowState win = newWindowBuilder(name, LAST_APPLICATION_WINDOW + 1).build(); win.setHasSurface(true); spyOn(win); return win; diff --git a/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java index bef4531c9f28..5122aeee588a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java @@ -239,6 +239,11 @@ public class LockTaskControllerTest { verifyLockTaskStarted(STATUS_BAR_MASK_PINNED, DISABLE2_NONE); // THEN screen pinning toast should be shown verify(mStatusBarService).showPinningEnterExitToast(eq(true /* entering */)); + + // WHEN the app calls startLockTaskMode while the Task is already locked + mLockTaskController.startLockTaskMode(tr, false, TEST_UID); + // THEN a pinning request should NOT be shown + verify(mStatusBarManagerInternal, never()).showScreenPinningRequest(anyInt(), anyInt()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java index 73e5f58fa7e0..45436e47e881 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java @@ -108,7 +108,7 @@ public class RefreshRatePolicyTest extends WindowTestsBase { } WindowState createWindow(String name) { - WindowState window = createWindow(null, TYPE_BASE_APPLICATION, name); + WindowState window = newWindowBuilder(name, TYPE_BASE_APPLICATION).build(); when(window.getDisplayInfo()).thenReturn(mDisplayInfo); when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) .thenReturn(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS); diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java index c5cbedb9193c..20381ba21758 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java @@ -114,8 +114,8 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { } private WindowState createAppOverlayWindow() { - final WindowState win = createWindow(null /* parent */, TYPE_APPLICATION_OVERLAY, - "testOverlayWindow"); + final WindowState win = newWindowBuilder("testOverlayWindow", + TYPE_APPLICATION_OVERLAY).build(); win.mActivityRecord = null; win.mHasSurface = true; return win; @@ -123,7 +123,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testForwardsShowBackdrop() throws Exception { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mDisplayContent.mOpeningApps.add(win.mActivityRecord); final WindowState overlayWin = createAppOverlayWindow(); try { @@ -156,7 +156,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testRun() throws Exception { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mDisplayContent.mOpeningApps.add(win.mActivityRecord); final WindowState overlayWin = createAppOverlayWindow(); try { @@ -200,7 +200,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testCancel() throws Exception { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); final AnimationAdapter adapter = mController.createRemoteAnimationRecord( win.mActivityRecord, new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; @@ -214,7 +214,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testTimeout() throws Exception { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); final AnimationAdapter adapter = mController.createRemoteAnimationRecord( win.mActivityRecord, new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; @@ -234,8 +234,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { public void testTimeout_scaled() throws Exception { try { mWm.setAnimationScale(2, 5.0f); - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, - "testWin"); + final WindowState win = createTestWindow(); final AnimationAdapter adapter = mController.createRemoteAnimationRecord( win.mActivityRecord, new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; @@ -268,7 +267,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testNotReallyStarted() throws Exception { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mController.createRemoteAnimationRecord(win.mActivityRecord, new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); @@ -278,8 +277,8 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testOneNotStarted() throws Exception { - final WindowState win1 = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin1"); - final WindowState win2 = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin2"); + final WindowState win1 = newWindowBuilder("testWin1", TYPE_BASE_APPLICATION).build(); + final WindowState win2 = newWindowBuilder("testWin2", TYPE_BASE_APPLICATION).build(); mController.createRemoteAnimationRecord(win1.mActivityRecord, new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false); final AnimationAdapter adapter = mController.createRemoteAnimationRecord( @@ -306,7 +305,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testRemovedBeforeStarted() throws Exception { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); final AnimationAdapter adapter = mController.createRemoteAnimationRecord( win.mActivityRecord, new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; @@ -322,7 +321,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testOpeningTaskWithTopFinishingActivity() { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "win"); + final WindowState win = createTestWindow(); final Task task = win.getTask(); final ActivityRecord topFinishing = new ActivityBuilder(mAtm).setTask(task).build(); // Now the task contains: @@ -348,7 +347,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testChangeToSmallerSize() throws Exception { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mDisplayContent.mChangingContainers.add(win.mActivityRecord); try { final RemoteAnimationRecord record = mController.createRemoteAnimationRecord( @@ -402,7 +401,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testChangeTolargerSize() throws Exception { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mDisplayContent.mChangingContainers.add(win.mActivityRecord); try { final RemoteAnimationRecord record = mController.createRemoteAnimationRecord( @@ -456,7 +455,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testChangeToDifferentPosition() throws Exception { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mDisplayContent.mChangingContainers.add(win.mActivityRecord); try { final RemoteAnimationRecord record = mController.createRemoteAnimationRecord( @@ -515,7 +514,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { true, mDisplayContent, true /* ownerCanManageAppTokens */); spyOn(mDisplayContent.mWallpaperController); doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible(); - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mDisplayContent.mOpeningApps.add(win.mActivityRecord); try { final AnimationAdapter adapter = mController.createRemoteAnimationRecord( @@ -548,7 +547,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { true, mDisplayContent, true /* ownerCanManageAppTokens */); spyOn(mDisplayContent.mWallpaperController); doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible(); - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mDisplayContent.mOpeningApps.add(win.mActivityRecord); try { final AnimationAdapter adapter = mController.createRemoteAnimationRecord( @@ -581,7 +580,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { @Test public void testNonAppIncluded_keygaurdGoingAway() throws Exception { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mDisplayContent.mOpeningApps.add(win.mActivityRecord); // Add overlay window hidden by the keyguard. final WindowState overlayWin = createAppOverlayWindow(); @@ -631,7 +630,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { true, mDisplayContent, true /* ownerCanManageAppTokens */); spyOn(mDisplayContent.mWallpaperController); doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible(); - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mDisplayContent.mOpeningApps.add(win.mActivityRecord); // Add overlay window hidden by the keyguard. final WindowState overlayWin = createAppOverlayWindow(); @@ -729,9 +728,9 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { } private AnimationAdapter setupForNonAppTargetNavBar(int transit, boolean shouldAttachNavBar) { - final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); + final WindowState win = createTestWindow(); mDisplayContent.mOpeningApps.add(win.mActivityRecord); - final WindowState navBar = createWindow(null, TYPE_NAVIGATION_BAR, "NavigationBar"); + final WindowState navBar = newWindowBuilder("NavigationBar", TYPE_NAVIGATION_BAR).build(); mDisplayContent.getDisplayPolicy().addWindowLw(navBar, navBar.mAttrs); final DisplayPolicy policy = mDisplayContent.getDisplayPolicy(); spyOn(policy); @@ -751,4 +750,8 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { verify(binder, atLeast(0)).asBinder(); verifyNoMoreInteractions(binder); } + + private WindowState createTestWindow() { + return newWindowBuilder("testWin", TYPE_BASE_APPLICATION).build(); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 6a738ae54dcd..9d9f24cb50f2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -673,7 +673,8 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); - final WindowState window = createWindow(null, TYPE_BASE_APPLICATION, mActivity, "window"); + final WindowState window = newWindowBuilder("window", TYPE_BASE_APPLICATION).setWindowToken( + mActivity).build(); assertEquals(window, mActivity.findMainWindow()); @@ -3996,8 +3997,8 @@ public class SizeCompatTests extends WindowTestsBase { resizeDisplay(display, 2200, 2280); display.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Simulate insets, final app bounds are (0, 0, 2200, 2130) - landscape. - final WindowState navbar = createWindow(null, TYPE_NAVIGATION_BAR, mDisplayContent, - "navbar"); + final WindowState navbar = newWindowBuilder("navbar", TYPE_NAVIGATION_BAR).setDisplay( + mDisplayContent).build(); final Binder owner = new Binder(); navbar.mAttrs.providedInsets = new InsetsFrameProvider[] { new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()) @@ -4030,8 +4031,8 @@ public class SizeCompatTests extends WindowTestsBase { resizeDisplay(display, 2200, 2280); display.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Simulate taskbar, final app bounds are (0, 0, 2200, 2130) - landscape - final WindowState navbar = createWindow(null, TYPE_NAVIGATION_BAR, mDisplayContent, - "navbar"); + final WindowState navbar = newWindowBuilder("navbar", TYPE_NAVIGATION_BAR).setDisplay( + mDisplayContent).build(); final Binder owner = new Binder(); navbar.mAttrs.providedInsets = new InsetsFrameProvider[] { new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()) @@ -4059,8 +4060,8 @@ public class SizeCompatTests extends WindowTestsBase { resizeDisplay(dc, 2200, 2280); dc.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Simulate taskbar, final app bounds are (0, 0, 2200, 2130) - landscape - final WindowState navbar = createWindow(null, TYPE_NAVIGATION_BAR, mDisplayContent, - "navbar"); + final WindowState navbar = newWindowBuilder("navbar", TYPE_NAVIGATION_BAR).setDisplay( + mDisplayContent).build(); final Binder owner = new Binder(); navbar.mAttrs.providedInsets = new InsetsFrameProvider[] { new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()) diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java index 1c32980aac91..da5210cfa7e6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java @@ -152,9 +152,9 @@ public class SyncEngineTests extends WindowTestsBase { final Task task = taskRoot.getTask(); final ActivityRecord translucentTop = new ActivityBuilder(mAtm).setTask(task) .setActivityTheme(android.R.style.Theme_Translucent).build(); - createWindow(null, TYPE_BASE_APPLICATION, taskRoot, "win"); - final WindowState startingWindow = createWindow(null, TYPE_APPLICATION_STARTING, - translucentTop, "starting"); + newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(taskRoot).build(); + final WindowState startingWindow = newWindowBuilder("starting", + TYPE_APPLICATION_STARTING).setWindowToken(translucentTop).build(); startingWindow.mStartingData = new SnapshotStartingData(mWm, null, 0); task.mSharedStartingData = startingWindow.mStartingData; task.prepareSync(); @@ -355,7 +355,7 @@ public class SyncEngineTests extends WindowTestsBase { assertEquals(SYNC_STATE_NONE, botChildWC.mSyncState); // If the appearance of window won't change after reparenting, its sync state can be kept. - final WindowState w = createWindow(null, TYPE_BASE_APPLICATION, "win"); + final WindowState w = newWindowBuilder("win", TYPE_BASE_APPLICATION).build(); parentWC.onRequestedOverrideConfigurationChanged(w.getConfiguration()); w.reparent(botChildWC, POSITION_TOP); parentWC.prepareSync(); @@ -435,7 +435,7 @@ public class SyncEngineTests extends WindowTestsBase { @Test public void testNonBlastMethod() { - mAppWindow = createWindow(null, TYPE_BASE_APPLICATION, "mAppWindow"); + mAppWindow = newWindowBuilder("mAppWindow", TYPE_BASE_APPLICATION).build(); final BLASTSyncEngine bse = createTestBLASTSyncEngine(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 35a2546fca1a..c0f251e06d17 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -52,6 +52,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.clearInvocations; @@ -1070,94 +1071,106 @@ public class TaskFragmentTest extends WindowTestsBase { @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test - public void testSetAdjacentTaskFragments() { + public void testAdjacentSetForTaskFragments() { final Task task = createTask(mDisplayContent); final TaskFragment tf0 = createTaskFragmentWithActivity(task); final TaskFragment tf1 = createTaskFragmentWithActivity(task); final TaskFragment tf2 = createTaskFragmentWithActivity(task); - final TaskFragment.AdjacentSet adjacentTfs = new TaskFragment.AdjacentSet(tf0, tf1, tf2); - assertFalse(tf0.hasAdjacentTaskFragment()); - - tf0.setAdjacentTaskFragments(adjacentTfs); - - assertSame(adjacentTfs, tf0.getAdjacentTaskFragments()); - assertSame(adjacentTfs, tf1.getAdjacentTaskFragments()); - assertSame(adjacentTfs, tf2.getAdjacentTaskFragments()); - assertTrue(tf0.hasAdjacentTaskFragment()); - assertTrue(tf1.hasAdjacentTaskFragment()); - assertTrue(tf2.hasAdjacentTaskFragment()); - - final TaskFragment.AdjacentSet adjacentTfs2 = new TaskFragment.AdjacentSet(tf0, tf1); - tf0.setAdjacentTaskFragments(adjacentTfs2); - - assertSame(adjacentTfs2, tf0.getAdjacentTaskFragments()); - assertSame(adjacentTfs2, tf1.getAdjacentTaskFragments()); - assertNull(tf2.getAdjacentTaskFragments()); - assertTrue(tf0.hasAdjacentTaskFragment()); - assertTrue(tf1.hasAdjacentTaskFragment()); - assertFalse(tf2.hasAdjacentTaskFragment()); + + // Can have two TFs adjacent, + new TaskFragment.AdjacentSet(tf0, tf1); + + // 3+ TFs adjacent is not yet supported. + assertThrows(IllegalArgumentException.class, + () -> new TaskFragment.AdjacentSet(tf0, tf1, tf2)); } @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test - public void testClearAdjacentTaskFragments() { - final Task task = createTask(mDisplayContent); - final TaskFragment tf0 = createTaskFragmentWithActivity(task); - final TaskFragment tf1 = createTaskFragmentWithActivity(task); - final TaskFragment tf2 = createTaskFragmentWithActivity(task); - final TaskFragment.AdjacentSet adjacentTfs = new TaskFragment.AdjacentSet(tf0, tf1, tf2); - tf0.setAdjacentTaskFragments(adjacentTfs); - - tf0.clearAdjacentTaskFragments(); + public void testSetAdjacentTaskFragments() { + final Task task0 = createTask(mDisplayContent); + final Task task1 = createTask(mDisplayContent); + final Task task2 = createTask(mDisplayContent); + final TaskFragment.AdjacentSet adjTasks = new TaskFragment.AdjacentSet(task0, task1, task2); + assertFalse(task0.hasAdjacentTaskFragment()); + + task0.setAdjacentTaskFragments(adjTasks); + + assertSame(adjTasks, task0.getAdjacentTaskFragments()); + assertSame(adjTasks, task1.getAdjacentTaskFragments()); + assertSame(adjTasks, task2.getAdjacentTaskFragments()); + assertTrue(task0.hasAdjacentTaskFragment()); + assertTrue(task1.hasAdjacentTaskFragment()); + assertTrue(task2.hasAdjacentTaskFragment()); + + final TaskFragment.AdjacentSet adjTasks2 = new TaskFragment.AdjacentSet(task0, task1); + task0.setAdjacentTaskFragments(adjTasks2); + + assertSame(adjTasks2, task0.getAdjacentTaskFragments()); + assertSame(adjTasks2, task1.getAdjacentTaskFragments()); + assertNull(task2.getAdjacentTaskFragments()); + assertTrue(task0.hasAdjacentTaskFragment()); + assertTrue(task1.hasAdjacentTaskFragment()); + assertFalse(task2.hasAdjacentTaskFragment()); + } - assertNull(tf0.getAdjacentTaskFragments()); - assertNull(tf1.getAdjacentTaskFragments()); - assertNull(tf2.getAdjacentTaskFragments()); - assertFalse(tf0.hasAdjacentTaskFragment()); - assertFalse(tf1.hasAdjacentTaskFragment()); - assertFalse(tf2.hasAdjacentTaskFragment()); + @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) + @Test + public void testClearAdjacentTaskFragments() { + final Task task0 = createTask(mDisplayContent); + final Task task1 = createTask(mDisplayContent); + final Task task2 = createTask(mDisplayContent); + final TaskFragment.AdjacentSet adjTasks = new TaskFragment.AdjacentSet(task0, task1, task2); + task0.setAdjacentTaskFragments(adjTasks); + + task0.clearAdjacentTaskFragments(); + + assertNull(task0.getAdjacentTaskFragments()); + assertNull(task1.getAdjacentTaskFragments()); + assertNull(task2.getAdjacentTaskFragments()); + assertFalse(task0.hasAdjacentTaskFragment()); + assertFalse(task1.hasAdjacentTaskFragment()); + assertFalse(task2.hasAdjacentTaskFragment()); } @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testRemoveFromAdjacentTaskFragments() { - final Task task = createTask(mDisplayContent); - final TaskFragment tf0 = createTaskFragmentWithActivity(task); - final TaskFragment tf1 = createTaskFragmentWithActivity(task); - final TaskFragment tf2 = createTaskFragmentWithActivity(task); - final TaskFragment.AdjacentSet adjacentTfs = new TaskFragment.AdjacentSet(tf0, tf1, tf2); - tf0.setAdjacentTaskFragments(adjacentTfs); - - tf0.removeFromAdjacentTaskFragments(); - - assertNull(tf0.getAdjacentTaskFragments()); - assertSame(adjacentTfs, tf1.getAdjacentTaskFragments()); - assertSame(adjacentTfs, tf2.getAdjacentTaskFragments()); - assertFalse(adjacentTfs.contains(tf0)); - assertTrue(tf1.isAdjacentTo(tf2)); - assertTrue(tf2.isAdjacentTo(tf1)); - assertFalse(tf1.isAdjacentTo(tf0)); - assertFalse(tf0.isAdjacentTo(tf1)); - assertFalse(tf0.isAdjacentTo(tf0)); - assertFalse(tf1.isAdjacentTo(tf1)); + final Task task0 = createTask(mDisplayContent); + final Task task1 = createTask(mDisplayContent); + final Task task2 = createTask(mDisplayContent); + final TaskFragment.AdjacentSet adjTasks = new TaskFragment.AdjacentSet(task0, task1, task2); + task0.setAdjacentTaskFragments(adjTasks); + + task0.removeFromAdjacentTaskFragments(); + + assertNull(task0.getAdjacentTaskFragments()); + assertSame(adjTasks, task1.getAdjacentTaskFragments()); + assertSame(adjTasks, task2.getAdjacentTaskFragments()); + assertFalse(adjTasks.contains(task0)); + assertTrue(task1.isAdjacentTo(task2)); + assertTrue(task2.isAdjacentTo(task1)); + assertFalse(task1.isAdjacentTo(task0)); + assertFalse(task0.isAdjacentTo(task1)); + assertFalse(task0.isAdjacentTo(task0)); + assertFalse(task1.isAdjacentTo(task1)); } @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testRemoveFromAdjacentTaskFragmentsWhenRemove() { - final Task task = createTask(mDisplayContent); - final TaskFragment tf0 = createTaskFragmentWithActivity(task); - final TaskFragment tf1 = createTaskFragmentWithActivity(task); - final TaskFragment tf2 = createTaskFragmentWithActivity(task); - final TaskFragment.AdjacentSet adjacentTfs = new TaskFragment.AdjacentSet(tf0, tf1, tf2); - tf0.setAdjacentTaskFragments(adjacentTfs); - - tf0.removeImmediately(); - - assertNull(tf0.getAdjacentTaskFragments()); - assertSame(adjacentTfs, tf1.getAdjacentTaskFragments()); - assertSame(adjacentTfs, tf2.getAdjacentTaskFragments()); - assertFalse(adjacentTfs.contains(tf0)); + final Task task0 = createTask(mDisplayContent); + final Task task1 = createTask(mDisplayContent); + final Task task2 = createTask(mDisplayContent); + final TaskFragment.AdjacentSet adjTasks = new TaskFragment.AdjacentSet(task0, task1, task2); + task0.setAdjacentTaskFragments(adjTasks); + + task0.removeImmediately(); + + assertNull(task0.getAdjacentTaskFragments()); + assertSame(adjTasks, task1.getAdjacentTaskFragments()); + assertSame(adjTasks, task2.getAdjacentTaskFragments()); + assertFalse(adjTasks.contains(task0)); } private WindowState createAppWindow(ActivityRecord app, String name) { diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 78f32c1a4f88..2ee34d3a4b36 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -433,8 +433,8 @@ public class TransitionTests extends WindowTestsBase { final WallpaperWindowToken wallpaperWindowToken = spy(new WallpaperWindowToken(mWm, mock(IBinder.class), true, mDisplayContent, true /* ownerCanManageAppTokens */)); - final WindowState wallpaperWindow = createWindow(null, TYPE_WALLPAPER, wallpaperWindowToken, - "wallpaperWindow"); + final WindowState wallpaperWindow = newWindowBuilder("wallpaperWindow", + TYPE_WALLPAPER).setWindowToken(wallpaperWindowToken).build(); wallpaperWindowToken.setVisibleRequested(false); transition.collect(wallpaperWindowToken); wallpaperWindowToken.setVisibleRequested(true); @@ -630,8 +630,8 @@ public class TransitionTests extends WindowTestsBase { // Make DA organized so we can check that they don't get included. WindowContainer parent = wallpaperWindowToken.getParent(); makeDisplayAreaOrganized(parent, mDisplayContent); - final WindowState wallpaperWindow = createWindow(null, TYPE_WALLPAPER, wallpaperWindowToken, - "wallpaperWindow"); + final WindowState wallpaperWindow = newWindowBuilder("wallpaperWindow", + TYPE_WALLPAPER).setWindowToken(wallpaperWindowToken).build(); wallpaperWindowToken.setVisibleRequested(false); transition.collect(wallpaperWindowToken); wallpaperWindowToken.setVisibleRequested(true); @@ -1114,15 +1114,15 @@ public class TransitionTests extends WindowTestsBase { // Simulate gesture navigation (non-movable) so it is not seamless. doReturn(false).when(displayPolicy).navigationBarCanMove(); final Task task = createActivityRecord(mDisplayContent).getTask(); - final WindowState statusBar = createWindow(null, TYPE_STATUS_BAR, "statusBar"); - final WindowState navBar = createWindow(null, TYPE_NAVIGATION_BAR, "navBar"); - final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_STATUS_BAR).build(); + final WindowState navBar = newWindowBuilder("navBar", TYPE_NAVIGATION_BAR).build(); + final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build(); final WindowToken decorToken = new WindowToken.Builder(mWm, mock(IBinder.class), TYPE_NAVIGATION_BAR_PANEL).setDisplayContent(mDisplayContent) .setRoundedCornerOverlay(true).build(); - final WindowState screenDecor = - createWindow(null, decorToken.windowType, decorToken, "screenDecor"); - final WindowState[] windows = { statusBar, navBar, ime, screenDecor }; + final WindowState screenDecor = newWindowBuilder("screenDecor", + decorToken.windowType).setWindowToken(decorToken).build(); + final WindowState[] windows = {statusBar, navBar, ime, screenDecor}; makeWindowVisible(windows); mDisplayContent.getDisplayPolicy().addWindowLw(statusBar, statusBar.mAttrs); mDisplayContent.getDisplayPolicy().addWindowLw(navBar, navBar.mAttrs); @@ -1191,7 +1191,7 @@ public class TransitionTests extends WindowTestsBase { } private void testShellRotationOpen(TestTransitionPlayer player) { - final WindowState statusBar = createWindow(null, TYPE_STATUS_BAR, "statusBar"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_STATUS_BAR).build(); makeWindowVisible(statusBar); mDisplayContent.getDisplayPolicy().addWindowLw(statusBar, statusBar.mAttrs); final ActivityRecord app = createActivityRecord(mDisplayContent); @@ -1239,7 +1239,7 @@ public class TransitionTests extends WindowTestsBase { } private void testFixedRotationOpen(TestTransitionPlayer player) { - final WindowState statusBar = createWindow(null, TYPE_STATUS_BAR, "statusBar"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_STATUS_BAR).build(); makeWindowVisible(statusBar); mDisplayContent.getDisplayPolicy().addWindowLw(statusBar, statusBar.mAttrs); final WindowState navBar = createNavBarWithProvidedInsets(mDisplayContent); @@ -2272,15 +2272,24 @@ public class TransitionTests extends WindowTestsBase { public void cleanUp(SurfaceControl.Transaction t) { } }); + assertEquals(WindowAnimator.PENDING_STATE_NONE, mWm.mAnimator.mPendingState); + app.startAnimation(app.getPendingTransaction(), mock(AnimationAdapter.class), + false /* hidden */, SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION); + assertEquals(WindowAnimator.PENDING_STATE_HAS_CHANGES, mWm.mAnimator.mPendingState); + final Task task = app.getTask(); transition.collect(task); + assertEquals(WindowAnimator.PENDING_STATE_NEED_APPLY, mWm.mAnimator.mPendingState); final Rect bounds = new Rect(task.getBounds()); Configuration c = new Configuration(task.getRequestedOverrideConfiguration()); bounds.inset(10, 10); c.windowConfiguration.setBounds(bounds); task.onRequestedOverrideConfigurationChanged(c); assertTrue(freezeCalls.contains(task)); - transition.abort(); + + transition.start(); + mWm.mSyncEngine.abort(transition.getSyncId()); + assertEquals(WindowAnimator.PENDING_STATE_NONE, mWm.mAnimator.mPendingState); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index eb89a9fb20c5..358448e709f3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -243,8 +243,8 @@ public class WallpaperControllerTests extends WindowTestsBase { final WindowState homeWindow = createWallpaperTargetWindow(dc); - WindowState otherWindow = createWindow(null /* parent */, TYPE_APPLICATION, dc, - "otherWindow"); + WindowState otherWindow = newWindowBuilder("otherWindow", TYPE_APPLICATION).setDisplay( + dc).build(); dc.mWallpaperController.adjustWallpaperWindows(); @@ -275,7 +275,7 @@ public class WallpaperControllerTests extends WindowTestsBase { public void testUpdateWallpaperTarget() { final DisplayContent dc = mDisplayContent; final WindowState homeWin = createWallpaperTargetWindow(dc); - final WindowState appWin = createWindow(null, TYPE_BASE_APPLICATION, "app"); + final WindowState appWin = newWindowBuilder("app", TYPE_BASE_APPLICATION).build(); appWin.mAttrs.flags |= FLAG_SHOW_WALLPAPER; makeWindowVisible(appWin); @@ -290,9 +290,9 @@ public class WallpaperControllerTests extends WindowTestsBase { public void testShowWhenLockedWallpaperTarget() { final WindowState wallpaperWindow = createWallpaperWindow(mDisplayContent); wallpaperWindow.mToken.asWallpaperToken().setShowWhenLocked(true); - final WindowState behind = createWindow(null, TYPE_BASE_APPLICATION, "behind"); - final WindowState topTranslucent = createWindow(null, TYPE_BASE_APPLICATION, - "topTranslucent"); + final WindowState behind = newWindowBuilder("behind", TYPE_BASE_APPLICATION).build(); + final WindowState topTranslucent = newWindowBuilder("topTranslucent", + TYPE_BASE_APPLICATION).build(); behind.mAttrs.width = behind.mAttrs.height = topTranslucent.mAttrs.width = topTranslucent.mAttrs.height = WindowManager.LayoutParams.MATCH_PARENT; topTranslucent.mAttrs.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; @@ -314,8 +314,8 @@ public class WallpaperControllerTests extends WindowTestsBase { // Only transient-launch transition will make notification shade as last resort target. // This verifies that regular transition won't choose invisible keyguard as the target. - final WindowState keyguard = createWindow(null /* parent */, - WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE, "keyguard"); + final WindowState keyguard = newWindowBuilder("keyguard", + WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE).build(); keyguard.mAttrs.flags |= FLAG_SHOW_WALLPAPER; registerTestTransitionPlayer(); final Transition transition = wallpaperWindow.mTransitionController.createTransition( @@ -568,8 +568,8 @@ public class WallpaperControllerTests extends WindowTestsBase { private WindowState createWallpaperWindow(DisplayContent dc) { final WindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, mock(IBinder.class), true /* explicit */, dc, true /* ownerCanManageAppTokens */); - return createWindow(null /* parent */, TYPE_WALLPAPER, wallpaperWindowToken, - "wallpaperWindow"); + return newWindowBuilder("wallpaperWindow", TYPE_WALLPAPER).setWindowToken( + wallpaperWindowToken).build(); } private WindowState createWallpaperTargetWindow(DisplayContent dc) { @@ -578,8 +578,8 @@ public class WallpaperControllerTests extends WindowTestsBase { .build(); homeActivity.setVisibility(true); - WindowState appWindow = createWindow(null /* parent */, TYPE_BASE_APPLICATION, - homeActivity, "wallpaperTargetWindow"); + WindowState appWindow = newWindowBuilder("wallpaperTargetWindow", + TYPE_BASE_APPLICATION).setWindowToken(homeActivity).build(); appWindow.getAttrs().flags |= FLAG_SHOW_WALLPAPER; appWindow.mHasSurface = true; spyOn(appWindow); |