summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/view/inputmethod/flags.aconfig11
-rw-r--r--core/java/android/widget/RemoteViews.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt11
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt1
-rw-r--r--packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt10
-rw-r--r--packages/SettingsLib/SliderPreference/src/com/android/settingslib/widget/SliderPreference.java4
-rw-r--r--packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/FlowTestUtil.kt50
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java12
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java113
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java51
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java105
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt7
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt349
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt18
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt82
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt26
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt194
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt3
-rw-r--r--packages/SystemUI/res/layout/status_bar_notification_shelf.xml4
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt40
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java90
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt87
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChipsContainer.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/NotificationShelfBackgroundView.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/NotificationShelfIconContainer.kt90
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java30
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt8
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/FakeMediaControllerFactory.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt31
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt8
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt2
-rw-r--r--services/core/java/com/android/server/wm/BLASTSyncEngine.java9
-rw-r--r--services/core/java/com/android/server/wm/SurfaceAnimator.java10
-rw-r--r--services/core/java/com/android/server/wm/WindowAnimator.java38
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java25
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java21
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java42
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java92
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TransitionTests.java11
59 files changed, 1631 insertions, 398 deletions
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/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/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/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/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/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/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/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/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/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/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/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index b0d9fcd4344b..8865a079733a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -1307,8 +1307,7 @@ private inline fun <T> computeValue(
val currentContent = currentContentState.contents.last()
- // The element is shared: interpolate between the value in fromContent and the value in
- // toContent.
+ // The element is shared: interpolate between the value in fromContent and toContent.
// TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
// elements follow the finger direction.
val isSharedElement = fromState != null && toState != null
@@ -1343,153 +1342,52 @@ private inline fun <T> computeValue(
// The content for which we compute the transformation. Note that this is not necessarily
// [currentContent] because [currentContent] could be a different content than the transition
// fromContent or toContent during interruptions or when a ancestor transition is running.
- val content: ContentKey
+ val transformationContentKey: ContentKey =
+ getTransformationContentKey(
+ isDisabledSharedElement = isSharedElement,
+ currentContent = currentContent,
+ layoutImpl = layoutImpl,
+ transition = transition,
+ element = element,
+ currentSceneState = currentSceneState,
+ )
// Get the transformed value, i.e. the target value at the beginning (for entering elements) or
// end (for leaving elements) of the transition.
- val contentState: Element.State
- when {
- isSharedElement -> {
- content = currentContent
- contentState = currentContentState
- }
- isAncestorTransition(layoutImpl, transition) -> {
- if (
- fromState != null &&
- transition.transformationSpec.hasTransformation(element.key, fromContent)
- ) {
- content = fromContent
- contentState = fromState
- } else if (
- toState != null &&
- transition.transformationSpec.hasTransformation(element.key, toContent)
- ) {
- content = toContent
- contentState = toState
- } else {
- throw IllegalStateException(
- "Ancestor transition is active but no transformation " +
- "spec was found. The ancestor transition should have only been selected " +
- "when a transformation for that element and content was defined."
- )
- }
- }
- currentSceneState != null && currentContent == transition.currentScene -> {
- content = currentContent
- contentState = currentSceneState
- }
- fromState != null -> {
- content = fromContent
- contentState = fromState
- }
- else -> {
- content = toContent
- contentState = toState!!
- }
- }
+ val targetState: Element.State = element.stateByContent.getValue(transformationContentKey)
+ val idleValue = contentValue(targetState)
val transformationWithRange =
- transformation(transition.transformationSpec.transformations(element.key, content))
-
- val previewTransformation =
- transition.previewTransformationSpec?.let {
- transformation(it.transformations(element.key, content))
- }
- if (previewTransformation != null) {
- val isInPreviewStage = transition.isInPreviewStage
-
- val idleValue = contentValue(contentState)
- val isEntering = content == toContent
- val previewTargetValue =
- with(
- previewTransformation.transformation.requireInterpolatedTransformation(
- element,
- transition,
- ) {
- "Custom transformations in preview specs should not be possible"
- }
- ) {
- layoutImpl.propertyTransformationScope.transform(
- content,
- element.key,
- transition,
- idleValue,
- )
- }
-
- val targetValueOrNull =
- transformationWithRange?.let { transformation ->
- with(
- transformation.transformation.requireInterpolatedTransformation(
- element,
- transition,
- ) {
- "Custom transformations are not allowed for properties with a preview"
- }
- ) {
- layoutImpl.propertyTransformationScope.transform(
- content,
- element.key,
- transition,
- idleValue,
- )
- }
- }
+ transformation(
+ transition.transformationSpec.transformations(element.key, transformationContentKey)
+ )
- // Make sure we don't read progress if values are the same and we don't need to interpolate,
- // so we don't invalidate the phase where this is read.
+ val isElementEntering =
when {
- isInPreviewStage && isEntering && previewTargetValue == targetValueOrNull ->
- return previewTargetValue
- isInPreviewStage && !isEntering && idleValue == previewTargetValue -> return idleValue
- previewTargetValue == targetValueOrNull && idleValue == previewTargetValue ->
- return idleValue
- else -> {}
+ transformationContentKey == toContent -> true
+ transformationContentKey == fromContent -> false
+ isAncestorTransition(layoutImpl, transition) ->
+ isEnteringAncestorTransition(layoutImpl, transition)
+ transformationContentKey == transition.currentScene -> toState == null
+ else -> transformationContentKey == toContent
}
- val previewProgress = transition.previewProgress
- // progress is not needed for all cases of the below when block, therefore read it lazily
- // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range
- val previewRangeProgress =
- previewTransformation.range?.progress(previewProgress) ?: previewProgress
-
- if (isInPreviewStage) {
- // if we're in the preview stage of the transition, interpolate between start state and
- // preview target state:
- return if (isEntering) {
- // i.e. in the entering case between previewTargetValue and targetValue (or
- // idleValue if no transformation is defined in the second stage transition)...
- lerp(previewTargetValue, targetValueOrNull ?: idleValue, previewRangeProgress)
- } else {
- // ...and in the exiting case between the idleValue and the previewTargetValue.
- lerp(idleValue, previewTargetValue, previewRangeProgress)
- }
+ val previewTransformation =
+ transition.previewTransformationSpec?.let {
+ transformation(it.transformations(element.key, transformationContentKey))
}
- // if we're in the second stage of the transition, interpolate between the state the
- // element was left at the end of the preview-phase and the target state:
- return if (isEntering) {
- // i.e. in the entering case between preview-end-state and the idleValue...
- lerp(
- lerp(previewTargetValue, targetValueOrNull ?: idleValue, previewRangeProgress),
- idleValue,
- transformationWithRange?.range?.progress(transition.progress) ?: transition.progress,
- )
- } else {
- if (targetValueOrNull == null) {
- // ... and in the exiting case, the element should remain in the preview-end-state
- // if no further transformation is defined in the second-stage transition...
- lerp(idleValue, previewTargetValue, previewRangeProgress)
- } else {
- // ...and otherwise it should be interpolated between preview-end-state and
- // targetValue
- lerp(
- lerp(idleValue, previewTargetValue, previewRangeProgress),
- targetValueOrNull,
- transformationWithRange.range?.progress(transition.progress)
- ?: transition.progress,
- )
- }
- }
+ if (previewTransformation != null) {
+ return computePreviewTransformationValue(
+ transition,
+ idleValue,
+ transformationContentKey,
+ isElementEntering,
+ previewTransformation,
+ element,
+ layoutImpl,
+ transformationWithRange,
+ lerp,
+ )
}
if (transformationWithRange == null) {
@@ -1504,7 +1402,7 @@ private inline fun <T> computeValue(
is CustomPropertyTransformation ->
return with(transformation) {
layoutImpl.propertyTransformationScope.transform(
- content,
+ transformationContentKey,
element.key,
transition,
transition.coroutineScope,
@@ -1515,11 +1413,10 @@ private inline fun <T> computeValue(
}
}
- val idleValue = contentValue(contentState)
val targetValue =
with(transformation) {
layoutImpl.propertyTransformationScope.transform(
- content,
+ transformationContentKey,
element.key,
transition,
idleValue,
@@ -1536,23 +1433,167 @@ private inline fun <T> computeValue(
// TODO(b/290184746): Make sure that we don't overflow transformations associated to a range.
val rangeProgress = transformationWithRange.range?.progress(progress) ?: progress
- // Interpolate between the value at rest and the value before entering/after leaving.
- val isEntering =
- when {
- content == toContent -> true
- content == fromContent -> false
- isAncestorTransition(layoutImpl, transition) ->
- isEnteringAncestorTransition(layoutImpl, transition)
- content == transition.currentScene -> toState == null
- else -> content == toContent
- }
- return if (isEntering) {
+ return if (isElementEntering) {
lerp(targetValue, idleValue, rangeProgress)
} else {
lerp(idleValue, targetValue, rangeProgress)
}
}
+private fun getTransformationContentKey(
+ isDisabledSharedElement: Boolean,
+ currentContent: ContentKey,
+ layoutImpl: SceneTransitionLayoutImpl,
+ transition: TransitionState.Transition,
+ element: Element,
+ currentSceneState: Element.State?,
+): ContentKey {
+ return when {
+ isDisabledSharedElement -> {
+ currentContent
+ }
+ isAncestorTransition(layoutImpl, transition) -> {
+ if (
+ element.stateByContent[transition.fromContent] != null &&
+ transition.transformationSpec.hasTransformation(
+ element.key,
+ transition.fromContent,
+ )
+ ) {
+ transition.fromContent
+ } else if (
+ element.stateByContent[transition.toContent] != null &&
+ transition.transformationSpec.hasTransformation(
+ element.key,
+ transition.toContent,
+ )
+ ) {
+ transition.toContent
+ } else {
+ throw IllegalStateException(
+ "Ancestor transition is active but no transformation " +
+ "spec was found. The ancestor transition should have only been selected " +
+ "when a transformation for that element and content was defined."
+ )
+ }
+ }
+ currentSceneState != null && currentContent == transition.currentScene -> {
+ currentContent
+ }
+ element.stateByContent[transition.fromContent] != null -> {
+ transition.fromContent
+ }
+ else -> {
+ transition.toContent
+ }
+ }
+}
+
+private inline fun <T> computePreviewTransformationValue(
+ transition: TransitionState.Transition,
+ idleValue: T,
+ transformationContentKey: ContentKey,
+ isEntering: Boolean,
+ previewTransformation: TransformationWithRange<PropertyTransformation<T>>,
+ element: Element,
+ layoutImpl: SceneTransitionLayoutImpl,
+ transformationWithRange: TransformationWithRange<PropertyTransformation<T>>?,
+ lerp: (T, T, Float) -> T,
+): T {
+ val isInPreviewStage = transition.isInPreviewStage
+
+ val previewTargetValue =
+ with(
+ previewTransformation.transformation.requireInterpolatedTransformation(
+ element,
+ transition,
+ ) {
+ "Custom transformations in preview specs should not be possible"
+ }
+ ) {
+ layoutImpl.propertyTransformationScope.transform(
+ transformationContentKey,
+ element.key,
+ transition,
+ idleValue,
+ )
+ }
+
+ val targetValueOrNull =
+ transformationWithRange?.let { transformation ->
+ with(
+ transformation.transformation.requireInterpolatedTransformation(
+ element,
+ transition,
+ ) {
+ "Custom transformations are not allowed for properties with a preview"
+ }
+ ) {
+ layoutImpl.propertyTransformationScope.transform(
+ transformationContentKey,
+ element.key,
+ transition,
+ idleValue,
+ )
+ }
+ }
+
+ // Make sure we don't read progress if values are the same and we don't need to interpolate,
+ // so we don't invalidate the phase where this is read.
+ when {
+ isInPreviewStage && isEntering && previewTargetValue == targetValueOrNull ->
+ return previewTargetValue
+ isInPreviewStage && !isEntering && idleValue == previewTargetValue -> return idleValue
+ previewTargetValue == targetValueOrNull && idleValue == previewTargetValue ->
+ return idleValue
+ else -> {}
+ }
+
+ val previewProgress = transition.previewProgress
+ // progress is not needed for all cases of the below when block, therefore read it lazily
+ // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range
+ val previewRangeProgress =
+ previewTransformation.range?.progress(previewProgress) ?: previewProgress
+
+ if (isInPreviewStage) {
+ // if we're in the preview stage of the transition, interpolate between start state and
+ // preview target state:
+ return if (isEntering) {
+ // i.e. in the entering case between previewTargetValue and targetValue (or
+ // idleValue if no transformation is defined in the second stage transition)...
+ lerp(previewTargetValue, targetValueOrNull ?: idleValue, previewRangeProgress)
+ } else {
+ // ...and in the exiting case between the idleValue and the previewTargetValue.
+ lerp(idleValue, previewTargetValue, previewRangeProgress)
+ }
+ }
+
+ // if we're in the second stage of the transition, interpolate between the state the
+ // element was left at the end of the preview-phase and the target state:
+ return if (isEntering) {
+ // i.e. in the entering case between preview-end-state and the idleValue...
+ lerp(
+ lerp(previewTargetValue, targetValueOrNull ?: idleValue, previewRangeProgress),
+ idleValue,
+ transformationWithRange?.range?.progress(transition.progress) ?: transition.progress,
+ )
+ } else {
+ if (targetValueOrNull == null) {
+ // ... and in the exiting case, the element should remain in the preview-end-state
+ // if no further transformation is defined in the second-stage transition...
+ lerp(idleValue, previewTargetValue, previewRangeProgress)
+ } else {
+ // ...and otherwise it should be interpolated between preview-end-state and
+ // targetValue
+ lerp(
+ lerp(idleValue, previewTargetValue, previewRangeProgress),
+ targetValueOrNull,
+ transformationWithRange.range?.progress(transition.progress) ?: transition.progress,
+ )
+ }
+ }
+}
+
private fun isAncestorTransition(
layoutImpl: SceneTransitionLayoutImpl,
transition: TransitionState.Transition,
@@ -1564,7 +1605,7 @@ private fun isAncestorTransition(
private fun isEnteringAncestorTransition(
layoutImpl: SceneTransitionLayoutImpl,
- transition: TransitionState.Transition
+ transition: TransitionState.Transition,
): Boolean {
return layoutImpl.ancestors.fastAny { it.inContent == transition.toContent }
}
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/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/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/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/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/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/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/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/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/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/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/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 78f32c1a4f88..143cf551a28e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -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