diff options
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 |