diff options
65 files changed, 1833 insertions, 768 deletions
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 6fedcbe50f30..b9255ecaf1b6 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -126,6 +126,7 @@ interface INotificationManager boolean areChannelsBypassingDnd(); ParceledListSlice getNotificationChannelsBypassingDnd(String pkg, int uid); ParceledListSlice getPackagesBypassingDnd(int userId); + List<String> getPackagesWithAnyChannels(int userId); boolean isPackagePaused(String pkg); void deleteNotificationHistoryItem(String pkg, int uid, long postedTime); boolean isPermissionFixed(String pkg, int userId); diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index a10b6ff39a37..9d8ab03982e6 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -308,6 +308,16 @@ flag { } flag { + name: "nm_binder_perf_get_apps_with_channels" + namespace: "systemui" + description: "Use a single binder call to get the set of apps with channels for a user" + bug: "362981561" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "no_sbnholder" namespace: "systemui" description: "removes sbnholder from NLS" diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index c00f31db1a38..08365c935626 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -982,8 +982,8 @@ public class UserManager { /** * Specifies if a user is disallowed from adding new users. This can only be set by device * owners or profile owners on the main user. The default value is <code>false</code>. - * <p> When the device is an organization-owned device provisioned with a managed profile, - * this restriction will be set as a base restriction which cannot be removed by any admin. + * <p> When the device is an organization-owned device, this restriction will be set as + * a base restriction which cannot be removed by any admin. * * <p>Holders of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MODIFY_USERS} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index 28227a1f8746..6be3c1f18b39 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -189,14 +189,16 @@ public class BubbleTransitions { * * 1. Start inflating the bubble view * 2. Once inflated (but not-yet visible), tell WM to do the shell-transition. - * 3. Transition becomes ready, so notify Launcher - * 4. Launcher responds with showExpandedView which calls continueExpand() to make view visible - * 5. Surface is created which kicks off actual animation + * 3. When the transition becomes ready, notify Launcher in parallel + * 4. Wait for surface to be created + * 5. Once surface is ready, animate the task to a bubble * - * So, constructor -> onInflated -> startAnimation -> continueExpand -> surfaceCreated. + * While the animation is pending, we keep a reference to the pending transition in the bubble. + * This allows us to check in other parts of the code that this bubble will be shown via the + * transition animation. * - * continueExpand and surfaceCreated are set-up to happen in either order, though, to support - * UX/timing adjustments. + * startAnimation, continueExpand and surfaceCreated are set-up to happen in either order, + * to support UX/timing adjustments. */ @VisibleForTesting class ConvertToBubble implements Transitions.TransitionHandler, BubbleTransition { @@ -209,9 +211,9 @@ public class BubbleTransitions { final Rect mStartBounds = new Rect(); SurfaceControl mSnapshot = null; TaskInfo mTaskInfo; - boolean mFinishedExpand = false; BubbleViewProvider mPriorBubble = null; + private final TransitionProgress mTransitionProgress = new TransitionProgress(); private SurfaceControl.Transaction mFinishT; private SurfaceControl mTaskLeash; @@ -359,12 +361,12 @@ public class BubbleTransitions { startTransaction.apply(); mTaskViewTransitions.onExternalDone(transition); + mTransitionProgress.setTransitionReady(); + startExpandAnim(); return true; } - @Override - public void continueExpand() { - mFinishedExpand = true; + private void startExpandAnim() { final boolean animate = mLayerView.canExpandView(mBubble); if (animate) { mPriorBubble = mLayerView.prepareConvertedView(mBubble); @@ -375,19 +377,25 @@ public class BubbleTransitions { mLayerView.removeView(priorView); mPriorBubble = null; } - if (!animate || mBubble.getTaskView().getSurfaceControl() != null) { + if (!animate || mTransitionProgress.isReadyToAnimate()) { playAnimation(animate); } } @Override + public void continueExpand() { + mTransitionProgress.setReadyToExpand(); + } + + @Override public void surfaceCreated() { + mTransitionProgress.setSurfaceReady(); mMainExecutor.execute(() -> { final TaskViewTaskController tvc = mBubble.getTaskView().getController(); final TaskViewRepository.TaskViewState state = mRepository.byTaskView(tvc); if (state == null) return; state.mVisible = true; - if (mFinishedExpand) { + if (mTransitionProgress.isReadyToAnimate()) { playAnimation(true /* animate */); } }); @@ -403,9 +411,6 @@ public class BubbleTransitions { mFinishWct = null; } - // Preparation is complete. - mBubble.setPreparingTransition(null); - if (animate) { mLayerView.animateConvert(startT, mStartBounds, mSnapshot, mTaskLeash, () -> { mFinishCb.onTransitionFinished(mFinishWct); @@ -417,6 +422,42 @@ public class BubbleTransitions { mFinishCb = null; } } + + /** + * Keeps track of internal state of different steps of this BubbleTransition. + */ + private class TransitionProgress { + private boolean mTransitionReady; + private boolean mReadyToExpand; + private boolean mSurfaceReady; + + void setTransitionReady() { + mTransitionReady = true; + onUpdate(); + } + + void setReadyToExpand() { + mReadyToExpand = true; + onUpdate(); + } + + void setSurfaceReady() { + mSurfaceReady = true; + onUpdate(); + } + + boolean isReadyToAnimate() { + // Animation only depends on transition and surface state + return mTransitionReady && mSurfaceReady; + } + + private void onUpdate() { + if (mTransitionReady && mReadyToExpand && mSurfaceReady) { + // Clear the transition from bubble when all the steps are ready + mBubble.setPreparingTransition(null); + } + } + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl index 8cdb8c4512a9..f8d84e4f3c21 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl @@ -59,11 +59,12 @@ oneway interface IRecentsAnimationRunner { void onAnimationStart(in IRecentsAnimationController controller, in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers, in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras, - in TransitionInfo info) = 2; + in @nullable TransitionInfo info) = 2; /** * Called when the task of an activity that has been started while the recents animation * was running becomes ready for control. */ - void onTasksAppeared(in RemoteAnimationTarget[] app) = 3; + void onTasksAppeared(in RemoteAnimationTarget[] app, + in @nullable TransitionInfo transitionInfo) = 3; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 7751741ae082..a969845fb8e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -1214,13 +1214,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // Since we're accepting the merge, update the finish transaction so that changes via // that transaction will be applied on top of those of the merged transitions mFinishTransaction = finishT; - // not using the incoming anim-only surfaces - info.releaseAnimSurfaces(); + boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(); + if (!passTransitionInfo) { + // not using the incoming anim-only surfaces + info.releaseAnimSurfaces(); + } if (appearedTargets != null) { try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: calling onTasksAppeared", mInstanceId); - mListener.onTasksAppeared(appearedTargets); + mListener.onTasksAppeared(appearedTargets, passTransitionInfo ? info : null); } catch (RemoteException e) { Slog.e(TAG, "Error sending appeared tasks to recents animation", e); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java index b5911bfa54f2..87ee4f58bfdd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java @@ -186,15 +186,20 @@ public class BubbleTransitionsTest extends ShellTestCase { verify(startT).setPosition(any(), eq(0f), eq(0f)); verify(mBubbleData).notificationEntryUpdated(eq(mBubble), anyBoolean(), anyBoolean()); - ctb.continueExpand(); clearInvocations(mBubble); verify(mBubble, never()).setPreparingTransition(any()); ctb.surfaceCreated(); - verify(mBubble).setPreparingTransition(isNull()); + // Check that preparing transition is not reset before continueExpand is called + verify(mBubble, never()).setPreparingTransition(any()); ArgumentCaptor<Runnable> animCb = ArgumentCaptor.forClass(Runnable.class); verify(mLayerView).animateConvert(any(), any(), any(), any(), animCb.capture()); + + // continueExpand is now called, check that preparing transition is cleared + ctb.continueExpand(); + verify(mBubble).setPreparingTransition(isNull()); + assertFalse(finishCalled[0]); animCb.getValue().run(); assertTrue(finishCalled[0]); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index 439be9155b26..fd5e567f69ed 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -34,6 +34,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -293,6 +294,31 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testMerge_openingTasks_callsOnTasksAppeared() throws Exception { + final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class); + TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build()) + .build(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).merge( + mergeTransitionInfo, + new StubTransaction(), + finishT, + transition, + mock(Transitions.TransitionFinishCallback.class)); + mMainExecutor.flushAll(); + + verify(animationRunner).onTasksAppeared( + /* appearedTargets= */ any(), eq(mergeTransitionInfo)); + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() { ActivityManager.RunningTaskInfo freeformTask = new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java index 74fd828f97ea..274fd3593918 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java @@ -124,6 +124,7 @@ final class WritableNamespacePrefixes { "privacy", "private_compute_services", "profcollect_native_boot", + "profiling_testing", "remote_auth", "remote_key_provisioning_native", "rollback", diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 08c3e91744b2..49cdec11e104 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -850,7 +850,7 @@ java_library { "androidx.test.uiautomator_uiautomator", "androidx.core_core-animation-testing", "androidx.test.ext.junit", - "inline-mockito-robolectric-prebuilt", + "inline-mockito5-robolectric-prebuilt", "mockito-kotlin-nodeps", "platform-parametric-runner-lib", "SystemUICustomizationTestUtils", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 910f71276376..511e54b9abff 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1258,13 +1258,6 @@ flag { } flag { - name: "glanceable_hub_back_action" - namespace: "systemui" - description: "Support back action from glanceable hub" - bug: "382771533" -} - -flag { name: "dream_overlay_updated_font" namespace: "systemui" description: "Flag to enable updated font settings for dream overlay" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt index 8b0c00535262..09db2d653326 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt @@ -19,10 +19,12 @@ package com.android.systemui.common.ui.compose import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.res.painterResource -import com.android.compose.ui.graphics.painter.rememberDrawablePainter +import androidx.core.graphics.drawable.toBitmap import com.android.systemui.common.shared.model.Icon /** @@ -35,7 +37,12 @@ fun Icon(icon: Icon, modifier: Modifier = Modifier, tint: Color = LocalContentCo val contentDescription = icon.contentDescription?.load() when (icon) { is Icon.Loaded -> { - Icon(rememberDrawablePainter(icon.drawable), contentDescription, modifier, tint) + Icon( + remember(icon.drawable) { icon.drawable.toBitmap().asImageBitmap() }, + contentDescription, + modifier, + tint, + ) } is Icon.Resource -> Icon(painterResource(icon.res), contentDescription, modifier, tint) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt index 4d238ac3798d..8c5fad3906ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.back.domain.interactor -import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.DeviceFlagsValueProvider @@ -32,7 +31,6 @@ import androidx.test.filters.SmallTest import com.android.internal.statusbar.IStatusBarService import com.android.systemui.Flags import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.domain.interactor.CommunalBackActionInteractor import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope @@ -93,7 +91,6 @@ class BackActionInteractorTest : SysuiTestCase() { @Mock private lateinit var onBackInvokedDispatcher: WindowOnBackInvokedDispatcher @Mock private lateinit var iStatusBarService: IStatusBarService @Mock private lateinit var headsUpManager: HeadsUpManager - @Mock private lateinit var communalBackActionInteractor: CommunalBackActionInteractor private val keyguardRepository = FakeKeyguardRepository() private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor by lazy { @@ -118,7 +115,6 @@ class BackActionInteractorTest : SysuiTestCase() { windowRootViewVisibilityInteractor, shadeBackActionInteractor, qsController, - communalBackActionInteractor, ) } @@ -297,19 +293,6 @@ class BackActionInteractorTest : SysuiTestCase() { verify(shadeBackActionInteractor).onBackProgressed(0.4f) } - @Test - @EnableFlags(Flags.FLAG_GLANCEABLE_HUB_BACK_ACTION) - fun onBackAction_communalCanBeDismissed_communalBackActionInteractorCalled() { - backActionInteractor.start() - windowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true) - powerInteractor.setAwakeForTest() - val callback = getBackInvokedCallback() - whenever(communalBackActionInteractor.canBeDismissed()).thenReturn(true) - callback.onBackInvoked() - - verify(communalBackActionInteractor).onBackPressed() - } - private fun getBackInvokedCallback(): OnBackInvokedCallback { testScope.runCurrent() val captor = argumentCaptor<OnBackInvokedCallback>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt deleted file mode 100644 index 70f38f7bc94e..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.communal.domain.interactor - -import android.platform.test.annotations.EnableFlags -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_COMMUNAL_HUB -import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.data.repository.communalSceneRepository -import com.android.systemui.communal.shared.model.CommunalScenes -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.kosmos.runCurrent -import com.android.systemui.kosmos.runTest -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class CommunalBackActionInteractorTest : SysuiTestCase() { - private val kosmos = testKosmos() - - private var Kosmos.underTest by Fixture { communalBackActionInteractor } - - @Test - @EnableFlags(FLAG_COMMUNAL_HUB) - fun communalShowing_canBeDismissed() = - kosmos.runTest { - setCommunalAvailable(true) - assertThat(underTest.canBeDismissed()).isEqualTo(false) - communalInteractor.changeScene(CommunalScenes.Communal, "test") - runCurrent() - assertThat(underTest.canBeDismissed()).isEqualTo(true) - } - - @Test - @EnableFlags(FLAG_COMMUNAL_HUB) - fun onBackPressed_invokesSceneChange() = - kosmos.runTest { - underTest.onBackPressed() - runCurrent() - assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank) - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt index feee9e3d62d2..6eace1b50ea7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt @@ -128,6 +128,19 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { } @Test + fun tutorialState_startedAndCommunalSceneShowing_stateWillNotUpdate() = + testScope.runTest { + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) + + goToCommunal() + + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_STARTED) + } + + @Test fun tutorialState_completedAndCommunalSceneShowing_stateWillNotUpdate() = testScope.runTest { val tutorialSettingState by diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt new file mode 100644 index 000000000000..052dfd52887f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2025 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.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DozingToDreamingTransitionViewModelTest : SysuiTestCase() { + val kosmos = testKosmos() + + val underTest by lazy { kosmos.dozingToDreamingTransitionViewModel } + + @Test + fun notificationShadeAlpha() = + kosmos.runTest { + val values by collectValues(underTest.notificationAlpha) + assertThat(values).isEmpty() + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.DOZING, + to = KeyguardState.DREAMING, + testScope, + ) + + assertThat(values).isNotEmpty() + values.forEach { assertThat(it).isEqualTo(0) } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/QuickStepContractTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/QuickStepContractTest.kt index d92781a5f3ce..ef03fab95778 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/QuickStepContractTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/QuickStepContractTest.kt @@ -16,10 +16,8 @@ package com.android.systemui.shared.system -import android.platform.test.annotations.DisableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_BACK_ACTION import com.android.systemui.SysuiTestCase import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING @@ -32,7 +30,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QuickStepContractTest : SysuiTestCase() { @Test - @DisableFlags(FLAG_GLANCEABLE_HUB_BACK_ACTION) fun isBackGestureDisabled_hubShowing() { val sysuiStateFlags = SYSUI_STATE_COMMUNAL_HUB_SHOWING diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 6f785a3731e1..dde6e2ee1866 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -65,11 +65,9 @@ import com.android.keyguard.KeyguardMessageArea; import com.android.keyguard.KeyguardMessageAreaController; import com.android.keyguard.KeyguardSecurityModel; import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; -import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.BouncerInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor; @@ -146,7 +144,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private SysuiStatusBarStateController mStatusBarStateController; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private View mNotificationContainer; - @Mock private KeyguardBypassController mBypassController; @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController; @Mock private KeyguardMessageArea mKeyguardMessageArea; @@ -158,7 +155,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; - @Mock private UdfpsOverlayInteractor mUdfpsOverlayInteractor; @Mock private ActivityStarter mActivityStarter; @Mock private BouncerView mBouncerView; @Mock private BouncerViewDelegate mBouncerViewDelegate; @@ -167,7 +163,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private NotificationShadeWindowView mNotificationShadeWindowView; @Mock private WindowInsetsController mWindowInsetsController; @Mock private TaskbarDelegate mTaskbarDelegate; - @Mock private StatusBarKeyguardViewManager.KeyguardViewManagerCallback mCallback; @Mock private SelectedUserInteractor mSelectedUserInteractor; @Mock private DeviceEntryInteractor mDeviceEntryInteractor; @Mock private SceneInteractor mSceneInteractor; @@ -190,8 +185,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { private KeyguardTransitionInteractor mKeyguardTransitionInteractor; @Captor private ArgumentCaptor<OnBackInvokedCallback> mBackCallbackCaptor; - @Captor - private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback; @Mock private KeyguardDismissActionInteractor mKeyguardDismissActionInteractor; @@ -227,7 +220,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mock(DockManager.class), mNotificationShadeWindowController, mKeyguardStateController, - mKeyguardMessageAreaFactory, Optional.of(mSysUiUnfoldComponent), () -> mShadeController, mLatencyTracker, @@ -236,7 +228,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mPrimaryBouncerInteractor, mBouncerView, mAlternateBouncerInteractor, - mUdfpsOverlayInteractor, mActivityStarter, mKeyguardTransitionInteractor, mock(KeyguardDismissTransitionInteractor.class), @@ -732,7 +723,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mock(DockManager.class), mock(NotificationShadeWindowController.class), mKeyguardStateController, - mKeyguardMessageAreaFactory, Optional.of(mSysUiUnfoldComponent), () -> mShadeController, mLatencyTracker, @@ -741,7 +731,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mPrimaryBouncerInteractor, mBouncerView, mAlternateBouncerInteractor, - mUdfpsOverlayInteractor, mActivityStarter, mock(KeyguardTransitionInteractor.class), mock(KeyguardDismissTransitionInteractor.class), diff --git a/packages/SystemUI/res/layout/battery_status_chip.xml b/packages/SystemUI/res/layout/battery_status_chip.xml index 74371839e247..7399651d4248 100644 --- a/packages/SystemUI/res/layout/battery_status_chip.xml +++ b/packages/SystemUI/res/layout/battery_status_chip.xml @@ -24,21 +24,13 @@ <LinearLayout android:id="@+id/rounded_container" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:minHeight="@dimen/ongoing_appops_chip_height" - android:layout_gravity="center" - android:background="@drawable/statusbar_chip_bg" - android:clipToOutline="true" - android:gravity="center" - android:maxWidth="@dimen/ongoing_appops_chip_max_width" - android:minWidth="@dimen/ongoing_appops_chip_min_width"> + style="@style/StatusBar.EventChip"> <com.android.systemui.battery.BatteryMeterView android:id="@+id/battery_meter_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginHorizontal="10dp" /> + android:layout_marginHorizontal="@dimen/ongoing_appops_chip_content_horizontal_margin" /> </LinearLayout> </merge>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/status_bar_event_chip_compose.xml b/packages/SystemUI/res/layout/status_bar_event_chip_compose.xml new file mode 100644 index 000000000000..ff96ab15cd15 --- /dev/null +++ b/packages/SystemUI/res/layout/status_bar_event_chip_compose.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> + +<merge xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="center_vertical|end"> + + <LinearLayout + android:id="@+id/rounded_container" + style="@style/StatusBar.EventChip"> + + <!-- Stub for the composable --> + <androidx.compose.ui.platform.ComposeView + android:id="@+id/compose_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/ongoing_appops_chip_content_horizontal_margin" /> + + </LinearLayout> +</merge> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c1fcbfd4de70..12a086939e53 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1243,7 +1243,7 @@ <dimen name="max_window_blur_radius">23px</dimen> <!-- Blur radius behind Notification Shade --> - <dimen name="max_shade_window_blur_radius">60dp</dimen> + <dimen name="max_shade_window_blur_radius">34dp</dimen> <!-- How much into a DisplayCutout's bounds we can go, on each side --> <dimen name="display_cutout_margin_consumption">0px</dimen> @@ -1254,6 +1254,8 @@ <dimen name="ongoing_appops_chip_side_padding">8dp</dimen> <!-- Margin between icons of Ongoing App Ops chip --> <dimen name="ongoing_appops_chip_icon_margin">4dp</dimen> + <!-- Side margins for the content of an appops chip --> + <dimen name="ongoing_appops_chip_content_horizontal_margin">10dp</dimen> <!-- Icon size of Ongoing App Ops chip --> <dimen name="ongoing_appops_chip_icon_size">16sp</dimen> <!-- Radius of Ongoing App Ops chip corners --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 4961a7ece69a..8f808d389203 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -93,6 +93,19 @@ <item name="android:textColor">?android:attr/colorPrimary</item> </style> + <style name="StatusBar.EventChip"> + <item name="android:orientation">horizontal</item> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_gravity">center</item> + <item name="android:gravity">center</item> + <item name="android:clipToOutline">true</item> + <item name="android:background">@drawable/statusbar_chip_bg</item> + <item name="android:minHeight">@dimen/ongoing_appops_chip_height</item> + <item name="android:maxWidth">@dimen/ongoing_appops_chip_max_width</item> + <item name="android:minWidth">@dimen/ongoing_appops_chip_min_width</item> + </style> + <style name="Chipbar" /> <style name="Chipbar.Text" parent="@*android:style/TextAppearance.DeviceDefault.Notification.Title"> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 82ac78c6db15..0372a6c6d2c5 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -20,7 +20,6 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; -import static com.android.systemui.Flags.glanceableHubBackAction; import static com.android.systemui.shared.Flags.shadeAllowBackGesture; import android.annotation.LongDef; @@ -361,10 +360,6 @@ public class QuickStepContract { } // Disable back gesture on the hub, but not when the shade is showing. if ((sysuiStateFlags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) { - // Allow back gesture on Glanceable Hub with back action support. - if (glanceableHubBackAction()) { - return false; - } // Use QS expanded signal as the notification panel is always considered visible // expanded when on the lock screen and when opening hub over lock screen. This does // mean that back gesture is disabled when opening shade over hub while in portrait diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java index ff6bcdb150f8..fcde508b07a8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java @@ -16,6 +16,7 @@ package com.android.systemui.shared.system; +import android.annotation.Nullable; import android.graphics.Rect; import android.os.Bundle; import android.view.RemoteAnimationTarget; @@ -42,5 +43,5 @@ public interface RecentsAnimationListener { * Called when the task of an activity that has been started while the recents animation * was running becomes ready for control. */ - void onTasksAppeared(RemoteAnimationTarget[] app); + void onTasksAppeared(RemoteAnimationTarget[] app, @Nullable TransitionInfo transitionInfo); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java index 5a9cbce73e4b..892851cd7056 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java @@ -173,6 +173,11 @@ public interface KeyguardViewController { boolean isBouncerShowing(); /** + * Report when the UI is ready for dismissing the whole Keyguard. + */ + void readyForKeyguardDone(); + + /** * Stop showing the alternate bouncer, if showing. * * <p>Should be like calling {@link #hideAlternateBouncer(boolean, boolean)} with a {@code true} diff --git a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt index 11a6cb9334ae..0b578c65e915 100644 --- a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt @@ -23,9 +23,7 @@ import android.window.OnBackInvokedDispatcher import android.window.WindowOnBackInvokedDispatcher import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable -import com.android.systemui.Flags.glanceableHubBackAction import com.android.systemui.Flags.predictiveBackAnimateShade -import com.android.systemui.communal.domain.interactor.CommunalBackActionInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -52,7 +50,6 @@ constructor( private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor, private val shadeBackActionInteractor: ShadeBackActionInteractor, private val qsController: QuickSettingsController, - private val communalBackActionInteractor: CommunalBackActionInteractor, ) : CoreStartable { private var isCallbackRegistered = false @@ -114,12 +111,6 @@ constructor( shadeBackActionInteractor.animateCollapseQs(false) return true } - if (glanceableHubBackAction()) { - if (communalBackActionInteractor.canBeDismissed()) { - communalBackActionInteractor.onBackPressed() - return true - } - } if (shouldBackBeHandled()) { if (shadeBackActionInteractor.canBeCollapsed()) { // this is the Shade dismiss animation, so make sure QQS closes when it ends. diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt deleted file mode 100644 index 2ccf96abff79..000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.communal.domain.interactor - -import com.android.systemui.communal.shared.model.CommunalScenes -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.scene.shared.model.Scenes -import javax.inject.Inject - -/** - * {@link CommunalBackActionInteractor} is responsible for handling back gestures on the glanceable - * hub. When invoked SystemUI should navigate back to the lockscreen. - */ -@SysUISingleton -class CommunalBackActionInteractor -@Inject -constructor( - private val communalInteractor: CommunalInteractor, - private val communalSceneInteractor: CommunalSceneInteractor, - private val sceneInteractor: SceneInteractor, -) { - fun canBeDismissed(): Boolean { - return communalInteractor.isCommunalShowing.value - } - - fun onBackPressed() { - if (SceneContainerFlag.isEnabled) { - // TODO(b/384610333): Properly determine whether to go to dream or lockscreen on back. - sceneInteractor.changeScene( - toScene = Scenes.Lockscreen, - loggingReason = "CommunalBackActionInteractor", - ) - } else { - communalSceneInteractor.changeScene( - newScene = CommunalScenes.Blank, - loggingReason = "CommunalBackActionInteractor", - ) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 6dab32a66c94..564628d3f52f 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -327,7 +327,7 @@ constructor( * use [isIdleOnCommunal]. */ // TODO(b/323215860): rename to something more appropriate after cleaning up usages - val isCommunalShowing: StateFlow<Boolean> = + val isCommunalShowing: Flow<Boolean> = flow { emit(SceneContainerFlag.isEnabled) } .flatMapLatest { sceneContainerEnabled -> if (sceneContainerEnabled) { @@ -345,10 +345,10 @@ constructor( columnName = "isCommunalShowing", initialValue = false, ) - .stateIn( + .shareIn( scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = false, + started = SharingStarted.WhileSubscribed(), + replay = 1, ) /** diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index a56a63c0b104..3132ec2b98e3 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -53,7 +53,6 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.PhoneWindow; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.systemui.Flags; import com.android.systemui.ambient.touch.TouchHandler; import com.android.systemui.ambient.touch.TouchMonitor; import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent; @@ -211,7 +210,6 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mCommunalVisible = communalVisible; updateLifecycleStateLocked(); - updateGestureBlockingLocked(); }); } }; @@ -594,8 +592,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private void updateGestureBlockingLocked() { final boolean shouldBlock = mStarted && !mShadeExpanded && !mBouncerShowing - && !isDreamInPreviewMode() - && !(Flags.glanceableHubBackAction() && mCommunalVisible); + && !isDreamInPreviewMode(); if (shouldBlock) { mGestureInteractor.addGestureBlockedMatcher(DREAM_TYPE_MATCHER, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 6f5f662d6fa3..0700ec639153 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -159,6 +159,7 @@ constructor( val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value + val canStartDreaming = dreamManager.canStartDreaming(false) if (!deviceEntryInteractor.isLockscreenEnabled()) { if (!SceneContainerFlag.isEnabled) { @@ -191,6 +192,13 @@ constructor( if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } + } else if (canStartDreaming) { + // If we're waking up to dream, transition directly to dreaming without + // showing the lockscreen. + startTransitionTo( + KeyguardState.DREAMING, + ownerReason = "moving from doze to dream", + ) } else { startTransitionTo(KeyguardState.LOCKSCREEN) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt index e6a85c6860c5..9018c58a7e36 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt @@ -39,4 +39,6 @@ constructor(animationFlow: KeyguardTransitionAnimationFlow) { ) val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) + // Notifications should not be shown while transitioning to dream. + val notificationAlpha = transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt index b271c6979b31..71977ef1f234 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt @@ -16,19 +16,19 @@ package com.android.systemui.shade +import com.android.keyguard.KeyguardViewController import com.android.systemui.assist.AssistManager import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.NotificationShadeWindowController -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import dagger.Lazy /** A base class for non-empty implementations of ShadeController. */ abstract class BaseShadeControllerImpl( protected val commandQueue: CommandQueue, - protected val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, + protected val keyguardViewController: KeyguardViewController, protected val notificationShadeWindowController: NotificationShadeWindowController, - protected val assistManagerLazy: Lazy<AssistManager> + protected val assistManagerLazy: Lazy<AssistManager>, ) : ShadeController { protected lateinit var notifPresenter: NotificationPresenter /** Runnables to run after completing a collapse of the shade. */ @@ -66,7 +66,7 @@ abstract class BaseShadeControllerImpl( for (r in clonedList) { r.run() } - statusBarKeyguardViewManager.readyForKeyguardDone() + keyguardViewController.readyForKeyguardDone() } final override fun onLaunchAnimationEnd(launchIsFullScreen: Boolean) { @@ -77,6 +77,7 @@ abstract class BaseShadeControllerImpl( instantCollapseShade() } } + final override fun onLaunchAnimationCancelled(isLaunchForActivity: Boolean) { if ( notifPresenter.isPresenterFullyCollapsed() && diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java index 0e30f2b4bb30..acae1bc81b97 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java @@ -23,6 +23,7 @@ import android.view.MotionEvent; import android.view.ViewTreeObserver; import android.view.WindowManagerGlobal; +import com.android.keyguard.KeyguardViewController; import com.android.systemui.DejankUtils; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; @@ -35,7 +36,6 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; @@ -61,7 +61,6 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { private final KeyguardStateController mKeyguardStateController; private final NotificationShadeWindowController mNotificationShadeWindowController; private final StatusBarStateController mStatusBarStateController; - private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final StatusBarWindowControllerStore mStatusBarWindowControllerStore; private final DeviceProvisionedController mDeviceProvisionedController; @@ -82,7 +81,7 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { WindowRootViewVisibilityInteractor windowRootViewVisibilityInteractor, KeyguardStateController keyguardStateController, StatusBarStateController statusBarStateController, - StatusBarKeyguardViewManager statusBarKeyguardViewManager, + KeyguardViewController keyguardViewController, StatusBarWindowControllerStore statusBarWindowControllerStore, DeviceProvisionedController deviceProvisionedController, NotificationShadeWindowController notificationShadeWindowController, @@ -93,7 +92,7 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { Lazy<NotificationGutsManager> gutsManager ) { super(commandQueue, - statusBarKeyguardViewManager, + keyguardViewController, notificationShadeWindowController, assistManagerLazy); SceneContainerFlag.assertInLegacyMode(); @@ -107,7 +106,6 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { mGutsManager = gutsManager; mNotificationShadeWindowController = notificationShadeWindowController; mNotifShadeWindowViewController = notificationShadeWindowViewController; - mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mDisplayId = displayId; mKeyguardStateController = keyguardStateController; mAssistManagerLazy = assistManagerLazy; @@ -396,7 +394,7 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { @Override public void collapseShadeForActivityStart() { - if (isExpandedVisible() && !mStatusBarKeyguardViewManager.isBouncerShowing()) { + if (isExpandedVisible() && !getKeyguardViewController().isBouncerShowing()) { animateCollapseShadeForcedDelayed(); } else { // Do it after DismissAction has been processed to conserve the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt index a58ce4162ddc..02cec13d2ce8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt @@ -26,6 +26,7 @@ import com.android.settingslib.flags.Flags.newStatusBarIcons import com.android.systemui.battery.BatteryMeterView import com.android.systemui.battery.unified.BatteryColors import com.android.systemui.res.R +import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.events.BackgroundAnimatableView class BatteryStatusChip @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : @@ -37,6 +38,8 @@ class BatteryStatusChip @JvmOverloads constructor(context: Context, attrs: Attri get() = batteryMeterView init { + NewStatusBarIcons.assertInLegacyMode() + inflate(context, R.layout.battery_status_chip, this) roundedContainer = requireViewById(R.id.rounded_container) batteryMeterView = requireViewById(R.id.battery_meter_view) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt index ea1d7820c79c..5887eb6ad865 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt @@ -25,6 +25,8 @@ import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyItem import com.android.systemui.statusbar.BatteryStatusChip import com.android.systemui.statusbar.ConnectedDisplayChip +import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.events.ui.view.BatteryStatusEventComposeChip typealias ViewCreator = (context: Context) -> BackgroundAnimatableView @@ -53,9 +55,7 @@ interface StatusEvent { } } -class BGView( - context: Context -) : View(context), BackgroundAnimatableView { +class BGView(context: Context) : View(context), BackgroundAnimatableView { override val view: View get() = this @@ -65,9 +65,7 @@ class BGView( } @SuppressLint("AppCompatCustomView") -class BGImageView( - context: Context -) : ImageView(context), BackgroundAnimatableView { +class BGImageView(context: Context) : ImageView(context), BackgroundAnimatableView { override val view: View get() = this @@ -84,8 +82,10 @@ class BatteryEvent(@IntRange(from = 0, to = 100) val batteryLevel: Int) : Status override val shouldAnnounceAccessibilityEvent: Boolean = false override val viewCreator: ViewCreator = { context -> - BatteryStatusChip(context).apply { - setBatteryLevel(batteryLevel) + if (NewStatusBarIcons.isEnabled) { + BatteryStatusEventComposeChip(batteryLevel, context) + } else { + BatteryStatusChip(context).apply { setBatteryLevel(batteryLevel) } } } @@ -103,9 +103,7 @@ class ConnectedDisplayEvent : StatusEvent { override var contentDescription: String? = "" override val shouldAnnounceAccessibilityEvent: Boolean = true - override val viewCreator: ViewCreator = { context -> - ConnectedDisplayChip(context) - } + override val viewCreator: ViewCreator = { context -> ConnectedDisplayChip(context) } override fun toString(): String { return javaClass.simpleName @@ -134,7 +132,8 @@ open class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEven } override fun shouldUpdateFromEvent(other: StatusEvent?): Boolean { - return other is PrivacyEvent && (other.privacyItems != privacyItems || + return other is PrivacyEvent && + (other.privacyItems != privacyItems || other.contentDescription != contentDescription || (other.forceVisible && !forceVisible)) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/ui/view/BatteryStatusEventComposeChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/ui/view/BatteryStatusEventComposeChip.kt new file mode 100644 index 000000000000..a90e3ff4b2dc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/ui/view/BatteryStatusEventComposeChip.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2025 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.events.ui.view + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import com.android.systemui.res.R +import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.events.BackgroundAnimatableView +import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryInteractor +import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryColors.LightThemeChargingColors +import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryFrame +import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryGlyph +import com.android.systemui.statusbar.pipeline.battery.ui.composable.BatteryCanvas +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel.Companion.glyphRepresentation + +/** + * [StatusEvent] chip for the battery plugged in status event. Shows the current battery level and + * charging state in the status bar via the system event animation. + * + * This chip will fully replace [BatteryStatusChip] when [NewStatusBarIcons] is rolled out + */ +@SuppressLint("ViewConstructor") +class BatteryStatusEventComposeChip +@JvmOverloads +constructor(level: Int, context: Context, attrs: AttributeSet? = null) : + FrameLayout(context, attrs), BackgroundAnimatableView { + private val roundedContainer: LinearLayout + private val composeInner: ComposeView + override val contentView: View + get() = composeInner + + init { + NewStatusBarIcons.assertInNewMode() + + inflate(context, R.layout.status_bar_event_chip_compose, this) + roundedContainer = requireViewById(R.id.rounded_container) + composeInner = requireViewById(R.id.compose_view) + composeInner.apply { + setContent { + val isFull = BatteryInteractor.isBatteryFull(level) + BatteryCanvas( + modifier = + Modifier.width(BatteryViewModel.STATUS_BAR_BATTERY_WIDTH) + .height(BatteryViewModel.STATUS_BAR_BATTERY_HEIGHT), + path = BatteryFrame.pathSpec, + // TODO(b/394659067): get a content description for this chip + contentDescription = "", + innerWidth = BatteryFrame.innerWidth, + innerHeight = BatteryFrame.innerHeight, + // This event only happens when plugged in, so we always show it as charging + glyphs = + if (isFull) listOf(BatteryGlyph.Bolt) + else level.glyphRepresentation() + BatteryGlyph.Bolt, + level = level, + isFull = isFull, + colorsProvider = { LightThemeChargingColors }, + ) + } + } + updateResources() + } + + /** + * When animating as a chip in the status bar, we want to animate the width for the rounded + * container. We have to subtract our own top and left offset because the bounds come to us as + * absolute on-screen bounds. + */ + override fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) { + roundedContainer.setLeftTopRightBottom(l - left, t - top, r - left, b - top) + } + + @SuppressLint("UseCompatLoadingForDrawables") + private fun updateResources() { + roundedContainer.background = mContext.getDrawable(R.drawable.statusbar_chip_bg) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 54efa4a2bcf2..2c8c7a1bdd44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -48,6 +48,7 @@ import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DozingToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel @@ -136,6 +137,7 @@ constructor( private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, private val aodToPrimaryBouncerTransitionViewModel: AodToPrimaryBouncerTransitionViewModel, + private val dozingToDreamingTransitionViewModel: DozingToDreamingTransitionViewModel, dozingToGlanceableHubTransitionViewModel: DozingToGlanceableHubTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, @@ -572,6 +574,7 @@ constructor( aodToLockscreenTransitionViewModel.notificationAlpha, aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), aodToPrimaryBouncerTransitionViewModel.notificationAlpha, + dozingToDreamingTransitionViewModel.notificationAlpha, dozingToLockscreenTransitionViewModel.lockscreenAlpha, dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToPrimaryBouncerTransitionViewModel.notificationAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 01de925f3d78..bc297699c41a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -46,7 +46,6 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; -import com.android.keyguard.KeyguardMessageAreaController; import com.android.keyguard.KeyguardSecurityModel; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; @@ -56,7 +55,6 @@ import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.DejankUtils; import com.android.systemui.Flags; import com.android.systemui.animation.back.FlingOnBackAnimationCallback; -import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.BouncerInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor; @@ -158,7 +156,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private final ConfigurationController mConfigurationController; private final NavigationModeController mNavigationModeController; private final NotificationShadeWindowController mNotificationShadeWindowController; - private final KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; private final DreamOverlayStateController mDreamOverlayStateController; @Nullable private final FoldAodAnimationController mFoldAodAnimationController; @@ -328,7 +325,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private float mQsExpansion; final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>(); - private final UdfpsOverlayInteractor mUdfpsOverlayInteractor; private final ActivityStarter mActivityStarter; private OnDismissAction mAfterKeyguardGoneAction; @@ -386,7 +382,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb DockManager dockManager, NotificationShadeWindowController notificationShadeWindowController, KeyguardStateController keyguardStateController, - KeyguardMessageAreaController.Factory keyguardMessageAreaFactory, Optional<SysUIUnfoldComponent> sysUIUnfoldComponent, Lazy<ShadeController> shadeController, LatencyTracker latencyTracker, @@ -395,7 +390,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb PrimaryBouncerInteractor primaryBouncerInteractor, BouncerView primaryBouncerView, AlternateBouncerInteractor alternateBouncerInteractor, - UdfpsOverlayInteractor udfpsOverlayInteractor, ActivityStarter activityStarter, KeyguardTransitionInteractor keyguardTransitionInteractor, KeyguardDismissTransitionInteractor keyguardDismissTransitionInteractor, @@ -423,7 +417,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardUpdateManager = keyguardUpdateMonitor; mStatusBarStateController = sysuiStatusBarStateController; mDockManager = dockManager; - mKeyguardMessageAreaFactory = keyguardMessageAreaFactory; mShadeController = shadeController; mLatencyTracker = latencyTracker; mKeyguardSecurityModel = keyguardSecurityModel; @@ -434,7 +427,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null); mAlternateBouncerInteractor = alternateBouncerInteractor; mBouncerInteractor = bouncerInteractor; - mUdfpsOverlayInteractor = udfpsOverlayInteractor; mActivityStarter = activityStarter; mKeyguardTransitionInteractor = keyguardTransitionInteractor; mKeyguardDismissTransitionInteractor = keyguardDismissTransitionInteractor; @@ -1581,6 +1573,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb && mPrimaryBouncerView.getDelegate().dispatchBackKeyEventPreIme(); } + @Override public void readyForKeyguardDone() { mViewMediatorCallback.readyForKeyguardDone(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt index 8fdb6ee57587..d53cbabb1d19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt @@ -29,7 +29,7 @@ class BatteryInteractor @Inject constructor(repo: BatteryRepository) { val level = repo.level.filterNotNull() /** Whether the battery has been fully charged */ - val isFull = level.map { it >= 100 } + val isFull = level.map { isBatteryFull(it) } /** * For the sake of battery views, consider it to be "charging" if plugged in. This allows users @@ -82,6 +82,8 @@ class BatteryInteractor @Inject constructor(repo: BatteryRepository) { companion object { /** Level below which we consider to be critically low */ private const val CRITICAL_LEVEL = 20 + + fun isBatteryFull(level: Int) = level >= 100 } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index 71ddcf65b7b6..98042d5022f9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -17,18 +17,20 @@ package com.android.systemui.volume.dialog.ui.binder import android.app.Dialog -import android.content.res.Resources import android.view.View import android.view.ViewTreeObserver import android.view.WindowInsets +import androidx.compose.ui.util.lerp import androidx.constraintlayout.motion.widget.MotionLayout import androidx.core.view.updatePadding +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FloatValueHolder +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce import com.android.internal.view.RotationPolicy import com.android.systemui.common.ui.view.onApplyWindowInsets -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.util.kotlin.awaitCancellationThenDispose -import com.android.systemui.volume.SystemUIInterpolators import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory @@ -36,6 +38,7 @@ import com.android.systemui.volume.dialog.ui.utils.suspendAnimate import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel import com.android.systemui.volume.dialog.utils.VolumeTracer import javax.inject.Inject +import kotlin.math.ceil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -47,24 +50,25 @@ import kotlinx.coroutines.flow.scan import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +private const val SPRING_STIFFNESS = 700f +private const val SPRING_DAMPING_RATIO = 0.9f + +private const val FRACTION_HIDE = 0f +private const val FRACTION_SHOW = 1f +private const val ANIMATION_MINIMUM_VISIBLE_CHANGE = 0.01f + /** Binds the root view of the Volume Dialog. */ @OptIn(ExperimentalCoroutinesApi::class) @VolumeDialogScope class VolumeDialogViewBinder @Inject constructor( - @Main resources: Resources, private val viewModel: VolumeDialogViewModel, private val jankListenerFactory: JankListenerFactory, private val tracer: VolumeTracer, private val viewBinders: List<@JvmSuppressWildcards ViewBinder>, ) { - private val dialogShowAnimationDurationMs = - resources.getInteger(R.integer.config_dialogShowAnimationDurationMs).toLong() - private val dialogHideAnimationDurationMs = - resources.getInteger(R.integer.config_dialogHideAnimationDurationMs).toLong() - fun CoroutineScope.bind(dialog: Dialog) { val insets: MutableStateFlow<WindowInsets> = MutableStateFlow(WindowInsets.Builder().build()) @@ -110,22 +114,35 @@ constructor( dialog: Dialog, visibilityModel: Flow<VolumeDialogVisibilityModel>, ) { + view.applyAnimationProgress(FRACTION_HIDE) + val animationValueHolder = FloatValueHolder(FRACTION_HIDE) + val animation: SpringAnimation = + SpringAnimation(animationValueHolder) + .setSpring( + SpringForce() + .setStiffness(SPRING_STIFFNESS) + .setDampingRatio(SPRING_DAMPING_RATIO) + ) + .setMinimumVisibleChange(ANIMATION_MINIMUM_VISIBLE_CHANGE) + .addUpdateListener { _, value, _ -> view.applyAnimationProgress(value) } + var junkListener: DynamicAnimation.OnAnimationUpdateListener? = null + visibilityModel .mapLatest { when (it) { is VolumeDialogVisibilityModel.Visible -> { tracer.traceVisibilityEnd(it) - view.animateShow( - duration = dialogShowAnimationDurationMs, - translationX = calculateTranslationX(view), - ) + junkListener?.let(animation::removeUpdateListener) + junkListener = + jankListenerFactory.show(view).also(animation::addUpdateListener) + animation.suspendAnimate(FRACTION_SHOW) } is VolumeDialogVisibilityModel.Dismissed -> { tracer.traceVisibilityEnd(it) - view.animateHide( - duration = dialogHideAnimationDurationMs, - translationX = calculateTranslationX(view), - ) + junkListener?.let(animation::removeUpdateListener) + junkListener = + jankListenerFactory.dismiss(view).also(animation::addUpdateListener) + animation.suspendAnimate(FRACTION_HIDE) dialog.dismiss() } is VolumeDialogVisibilityModel.Invisible -> { @@ -136,37 +153,21 @@ constructor( .launchIn(this) } - private fun calculateTranslationX(view: View): Float? { - return if (view.display.rotation == RotationPolicy.NATURAL_ROTATION) { - if (view.isLayoutRtl) { - -1 + /** + * @param fraction in range [0, 1]. 0 corresponds to the dialog being hidden and 1 - visible. + */ + private fun View.applyAnimationProgress(fraction: Float) { + alpha = ceil(fraction) + if (display.rotation == RotationPolicy.NATURAL_ROTATION) { + if (isLayoutRtl) { + -1 + } else { + 1 + } * width / 2f } else { - 1 - } * view.width / 2f - } else { - null - } - } - - private suspend fun View.animateShow(duration: Long, translationX: Float?) { - translationX?.let { setTranslationX(translationX) } - alpha = 0f - animate() - .alpha(1f) - .translationX(0f) - .setDuration(duration) - .setInterpolator(SystemUIInterpolators.LogDecelerateInterpolator()) - .suspendAnimate(jankListenerFactory.show(this, duration)) - } - - private suspend fun View.animateHide(duration: Long, translationX: Float?) { - val animator = - animate() - .alpha(0f) - .setDuration(duration) - .setInterpolator(SystemUIInterpolators.LogAccelerateInterpolator()) - translationX?.let { animator.translationX(it) } - animator.suspendAnimate(jankListenerFactory.dismiss(this, duration)) + null + } + ?.let { maxTranslationX -> translationX = lerp(maxTranslationX, 0f, fraction) } } private suspend fun ViewTreeObserver.listenToComputeInternalInsets() = diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt index 9fcd77716fb6..803911a9cf77 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt @@ -17,8 +17,8 @@ package com.android.systemui.volume.dialog.ui.utils import android.animation.Animator -import android.animation.AnimatorListenerAdapter import android.view.View +import androidx.dynamicanimation.animation.DynamicAnimation import com.android.internal.jank.Cuj import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope @@ -30,35 +30,36 @@ class JankListenerFactory @Inject constructor(private val interactionJankMonitor: InteractionJankMonitor) { - fun show(view: View, timeout: Long) = getJunkListener(view, "show", timeout) - - fun update(view: View, timeout: Long) = getJunkListener(view, "update", timeout) + fun show(view: View): DynamicAnimation.OnAnimationUpdateListener { + return createJunkListener(view, "show") + } - fun dismiss(view: View, timeout: Long) = getJunkListener(view, "dismiss", timeout) + fun dismiss(view: View): DynamicAnimation.OnAnimationUpdateListener { + return createJunkListener(view, "dismiss") + } - private fun getJunkListener( + private fun createJunkListener( view: View, type: String, - timeout: Long, - ): Animator.AnimatorListener { - return object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { + ): DynamicAnimation.OnAnimationUpdateListener { + var trackedStart = false + return DynamicAnimation.OnAnimationUpdateListener { animation, _, _ -> + if (!trackedStart) { + trackedStart = true interactionJankMonitor.begin( InteractionJankMonitor.Configuration.Builder.withView( Cuj.CUJ_VOLUME_CONTROL, view, ) .setTag(type) - .setTimeout(timeout) ) - } - - override fun onAnimationEnd(animation: Animator) { - interactionJankMonitor.end(Cuj.CUJ_VOLUME_CONTROL) - } - - override fun onAnimationCancel(animation: Animator) { - interactionJankMonitor.cancel(Cuj.CUJ_VOLUME_CONTROL) + animation.addEndListener { _, canceled, _, _ -> + if (canceled) { + interactionJankMonitor.cancel(Cuj.CUJ_VOLUME_CONTROL) + } else { + interactionJankMonitor.end(Cuj.CUJ_VOLUME_CONTROL) + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt index 52a19e0903e2..31e596f0278d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt @@ -96,21 +96,23 @@ suspend fun <T> ValueAnimator.suspendAnimate(onValueChanged: (T) -> Unit) { * Starts spring animation and suspends until it's finished. Cancels the animation if the running * coroutine is cancelled. */ -suspend fun SpringAnimation.suspendAnimate(onAnimationUpdate: (Float) -> Unit) = - suspendCancellableCoroutine { continuation -> - val updateListener = - DynamicAnimation.OnAnimationUpdateListener { _, value, _ -> onAnimationUpdate(value) } - val endListener = - DynamicAnimation.OnAnimationEndListener { _, _, _, _ -> continuation.resumeIfCan(Unit) } - addUpdateListener(updateListener) - addEndListener(endListener) - animateToFinalPosition(1F) - continuation.invokeOnCancellation { - removeUpdateListener(updateListener) - removeEndListener(endListener) - cancel() - } +suspend fun SpringAnimation.suspendAnimate( + finalPosition: Float = 1f, + onAnimationUpdate: (Float) -> Unit = {}, +) = suspendCancellableCoroutine { continuation -> + val updateListener = + DynamicAnimation.OnAnimationUpdateListener { _, value, _ -> onAnimationUpdate(value) } + val endListener = + DynamicAnimation.OnAnimationEndListener { _, _, _, _ -> continuation.resumeIfCan(Unit) } + addUpdateListener(updateListener) + addEndListener(endListener) + animateToFinalPosition(finalPosition) + continuation.invokeOnCancellation { + removeUpdateListener(updateListener) + removeEndListener(endListener) + cancel() } +} /** * Starts the animation and suspends until it's finished. Cancels the animation if the running diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt deleted file mode 100644 index 57c8fd066ea8..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.communal.domain.interactor - -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.scene.domain.interactor.sceneInteractor - -val Kosmos.communalBackActionInteractor by - Kosmos.Fixture { - CommunalBackActionInteractor( - communalInteractor = communalInteractor, - communalSceneInteractor = communalSceneInteractor, - sceneInteractor = sceneInteractor, - ) - } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index 7a2b7c24252b..047bd13f0c27 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -29,6 +29,7 @@ import com.android.systemui.keyguard.ui.viewmodel.aodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.dozingToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel @@ -81,6 +82,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, aodToPrimaryBouncerTransitionViewModel = aodToPrimaryBouncerTransitionViewModel, + dozingToDreamingTransitionViewModel = dozingToDreamingTransitionViewModel, dozingToGlanceableHubTransitionViewModel = dozingToGlanceableHubTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt index da32095dce6d..386e0feb3b3a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.volume.dialog.ui.binder -import android.content.applicationContext import com.android.systemui.kosmos.Kosmos import com.android.systemui.volume.dialog.ringer.volumeDialogRingerViewBinder import com.android.systemui.volume.dialog.settings.ui.binder.volumeDialogSettingsButtonViewBinder @@ -28,7 +27,6 @@ import com.android.systemui.volume.dialog.utils.volumeTracer val Kosmos.volumeDialogViewBinder by Kosmos.Fixture { VolumeDialogViewBinder( - applicationContext.resources, volumeDialogViewModel, jankListenerFactory, volumeTracer, diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 7e8bb28b6a37..2af74f620c95 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -569,7 +569,8 @@ public final class DreamManagerService extends SystemService { } private void requestDreamInternal() { - if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront()) { + if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront() + && !isDozingInternal()) { return; } diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index b0ef80793cd7..9ed9b6e56f13 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -25,6 +25,8 @@ import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND; +import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER; +import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser; import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled; import android.annotation.FlaggedApi; @@ -75,7 +77,9 @@ import com.android.internal.util.XmlUtils; import com.android.internal.util.function.TriPredicate; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.LocalServices; import com.android.server.notification.NotificationManagerService.DumpFilter; +import com.android.server.pm.UserManagerInternal; import com.android.server.utils.TimingsTraceAndSlog; import org.xmlpull.v1.XmlPullParser; @@ -134,6 +138,7 @@ abstract public class ManagedServices { private final UserProfiles mUserProfiles; protected final IPackageManager mPm; protected final UserManager mUm; + protected final UserManagerInternal mUmInternal; private final Config mConfig; private final Handler mHandler = new Handler(Looper.getMainLooper()); @@ -157,12 +162,17 @@ abstract public class ManagedServices { protected final ArraySet<String> mDefaultPackages = new ArraySet<>(); // lists the component names of all enabled (and therefore potentially connected) - // app services for current profiles. + // app services for each user. This is intended to support a concurrent multi-user environment. + // key value is the resolved userId. @GuardedBy("mMutex") - private final ArraySet<ComponentName> mEnabledServicesForCurrentProfiles = new ArraySet<>(); - // Just the packages from mEnabledServicesForCurrentProfiles + private final SparseArray<ArraySet<ComponentName>> mEnabledServicesByUser = + new SparseArray<>(); + // Just the packages from mEnabledServicesByUser + // This is intended to support a concurrent multi-user environment. + // key value is the resolved userId. @GuardedBy("mMutex") - private final ArraySet<String> mEnabledServicesPackageNames = new ArraySet<>(); + private final SparseArray<ArraySet<String>> mEnabledServicesPackageNamesByUser = + new SparseArray<>(); // Per user id, list of enabled packages that have nevertheless asked not to be run @GuardedBy("mSnoozing") private final SparseSetArray<ComponentName> mSnoozing = new SparseSetArray<>(); @@ -195,6 +205,7 @@ abstract public class ManagedServices { mConfig = getConfig(); mApprovalLevel = APPROVAL_BY_COMPONENT; mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mUmInternal = LocalServices.getService(UserManagerInternal.class); } abstract protected Config getConfig(); @@ -383,11 +394,30 @@ abstract public class ManagedServices { } synchronized (mMutex) { - pw.println(" All " + getCaption() + "s (" + mEnabledServicesForCurrentProfiles.size() - + ") enabled for current profiles:"); - for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) { - if (filter != null && !filter.matches(cmpt)) continue; - pw.println(" " + cmpt); + if (managedServicesConcurrentMultiuser()) { + for (int i = 0; i < mEnabledServicesByUser.size(); i++) { + final int userId = mEnabledServicesByUser.keyAt(i); + final ArraySet<ComponentName> componentNames = + mEnabledServicesByUser.get(userId); + String userString = userId == UserHandle.USER_CURRENT + ? "current profiles" : "user " + Integer.toString(userId); + pw.println(" All " + getCaption() + "s (" + componentNames.size() + + ") enabled for " + userString + ":"); + for (ComponentName cmpt : componentNames) { + if (filter != null && !filter.matches(cmpt)) continue; + pw.println(" " + cmpt); + } + } + } else { + final ArraySet<ComponentName> enabledServicesForCurrentProfiles = + mEnabledServicesByUser.get(UserHandle.USER_CURRENT); + pw.println(" All " + getCaption() + "s (" + + enabledServicesForCurrentProfiles.size() + + ") enabled for current profiles:"); + for (ComponentName cmpt : enabledServicesForCurrentProfiles) { + if (filter != null && !filter.matches(cmpt)) continue; + pw.println(" " + cmpt); + } } pw.println(" Live " + getCaption() + "s (" + mServices.size() + "):"); @@ -442,11 +472,24 @@ abstract public class ManagedServices { } } - synchronized (mMutex) { - for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) { - if (filter != null && !filter.matches(cmpt)) continue; - cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED); + if (managedServicesConcurrentMultiuser()) { + for (int i = 0; i < mEnabledServicesByUser.size(); i++) { + final int userId = mEnabledServicesByUser.keyAt(i); + final ArraySet<ComponentName> componentNames = + mEnabledServicesByUser.get(userId); + for (ComponentName cmpt : componentNames) { + if (filter != null && !filter.matches(cmpt)) continue; + cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED); + } + } + } else { + final ArraySet<ComponentName> enabledServicesForCurrentProfiles = + mEnabledServicesByUser.get(UserHandle.USER_CURRENT); + for (ComponentName cmpt : enabledServicesForCurrentProfiles) { + if (filter != null && !filter.matches(cmpt)) continue; + cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED); + } } for (ManagedServiceInfo info : mServices) { if (filter != null && !filter.matches(info.component)) continue; @@ -841,9 +884,31 @@ abstract public class ManagedServices { } } + /** convenience method for looking in mEnabledServicesPackageNamesByUser + * for UserHandle.USER_CURRENT. + * This is a legacy API. When FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER becomes + * trunk stable, this API should be deprecated. Additionally, when this method + * is deprecated, the unit tests written using this method should also be revised. + * + * @param pkg target package name + * @return boolean value that indicates whether it is enabled for the current profiles + */ protected boolean isComponentEnabledForPackage(String pkg) { + return isComponentEnabledForPackage(pkg, UserHandle.USER_CURRENT); + } + + /** convenience method for looking in mEnabledServicesPackageNamesByUser + * + * @param pkg target package name + * @param userId the id of the target user + * @return boolean value that indicates whether it is enabled for the target user + */ + @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + protected boolean isComponentEnabledForPackage(String pkg, int userId) { synchronized (mMutex) { - return mEnabledServicesPackageNames.contains(pkg); + ArraySet<String> enabledServicesPackageNames = + mEnabledServicesPackageNamesByUser.get(resolveUserId(userId)); + return enabledServicesPackageNames != null && enabledServicesPackageNames.contains(pkg); } } @@ -1016,9 +1081,14 @@ abstract public class ManagedServices { public void onPackagesChanged(boolean removingPackage, String[] pkgList, int[] uidList) { if (DEBUG) { synchronized (mMutex) { + int resolvedUserId = (managedServicesConcurrentMultiuser() + && (uidList != null && uidList.length > 0)) + ? resolveUserId(UserHandle.getUserId(uidList[0])) + : UserHandle.USER_CURRENT; Slog.d(TAG, "onPackagesChanged removingPackage=" + removingPackage + " pkgList=" + (pkgList == null ? null : Arrays.asList(pkgList)) - + " mEnabledServicesPackageNames=" + mEnabledServicesPackageNames); + + " mEnabledServicesPackageNames=" + + mEnabledServicesPackageNamesByUser.get(resolvedUserId)); } } @@ -1034,11 +1104,18 @@ abstract public class ManagedServices { } } for (String pkgName : pkgList) { - if (isComponentEnabledForPackage(pkgName)) { - anyServicesInvolved = true; + if (!managedServicesConcurrentMultiuser()) { + if (isComponentEnabledForPackage(pkgName)) { + anyServicesInvolved = true; + } } if (uidList != null && uidList.length > 0) { for (int uid : uidList) { + if (managedServicesConcurrentMultiuser()) { + if (isComponentEnabledForPackage(pkgName, UserHandle.getUserId(uid))) { + anyServicesInvolved = true; + } + } if (isPackageAllowed(pkgName, UserHandle.getUserId(uid))) { anyServicesInvolved = true; trimApprovedListsForInvalidServices(pkgName, UserHandle.getUserId(uid)); @@ -1065,6 +1142,36 @@ abstract public class ManagedServices { unbindUserServices(user); } + /** + * Call this method when a user is stopped + * + * @param user the id of the stopped user + */ + public void onUserStopped(int user) { + if (!managedServicesConcurrentMultiuser()) { + return; + } + boolean hasAny = false; + synchronized (mMutex) { + if (mEnabledServicesByUser.contains(user) + && mEnabledServicesPackageNamesByUser.contains(user)) { + // Through the ManagedServices.resolveUserId, + // we resolve UserHandle.USER_CURRENT as the key for users + // other than the visible background user. + // Therefore, the user IDs that exist as keys for each member variable + // correspond to the visible background user. + // We need to unbind services of the stopped visible background user. + mEnabledServicesByUser.remove(user); + mEnabledServicesPackageNamesByUser.remove(user); + hasAny = true; + } + } + if (hasAny) { + Slog.i(TAG, "Removing approved services for stopped user " + user); + unbindUserServices(user); + } + } + public void onUserSwitched(int user) { if (DEBUG) Slog.d(TAG, "onUserSwitched u=" + user); unbindOtherUserServices(user); @@ -1386,19 +1493,42 @@ abstract public class ManagedServices { protected void populateComponentsToBind(SparseArray<Set<ComponentName>> componentsToBind, final IntArray activeUsers, SparseArray<ArraySet<ComponentName>> approvedComponentsByUser) { - mEnabledServicesForCurrentProfiles.clear(); - mEnabledServicesPackageNames.clear(); final int nUserIds = activeUsers.size(); - + if (managedServicesConcurrentMultiuser()) { + for (int i = 0; i < nUserIds; ++i) { + final int resolvedUserId = resolveUserId(activeUsers.get(i)); + if (mEnabledServicesByUser.get(resolvedUserId) != null) { + mEnabledServicesByUser.get(resolvedUserId).clear(); + } + if (mEnabledServicesPackageNamesByUser.get(resolvedUserId) != null) { + mEnabledServicesPackageNamesByUser.get(resolvedUserId).clear(); + } + } + } else { + mEnabledServicesByUser.clear(); + mEnabledServicesPackageNamesByUser.clear(); + } for (int i = 0; i < nUserIds; ++i) { - // decode the list of components final int userId = activeUsers.get(i); + // decode the list of components final ArraySet<ComponentName> userComponents = approvedComponentsByUser.get(userId); if (null == userComponents) { componentsToBind.put(userId, new ArraySet<>()); continue; } + final int resolvedUserId = managedServicesConcurrentMultiuser() + ? resolveUserId(userId) + : UserHandle.USER_CURRENT; + ArraySet<ComponentName> enabledServices = + mEnabledServicesByUser.contains(resolvedUserId) + ? mEnabledServicesByUser.get(resolvedUserId) + : new ArraySet<>(); + ArraySet<String> enabledServicesPackageName = + mEnabledServicesPackageNamesByUser.contains(resolvedUserId) + ? mEnabledServicesPackageNamesByUser.get(resolvedUserId) + : new ArraySet<>(); + final Set<ComponentName> add = new HashSet<>(userComponents); synchronized (mSnoozing) { ArraySet<ComponentName> snoozed = mSnoozing.get(userId); @@ -1409,12 +1539,12 @@ abstract public class ManagedServices { componentsToBind.put(userId, add); - mEnabledServicesForCurrentProfiles.addAll(userComponents); - + enabledServices.addAll(userComponents); for (int j = 0; j < userComponents.size(); j++) { - final ComponentName component = userComponents.valueAt(j); - mEnabledServicesPackageNames.add(component.getPackageName()); + enabledServicesPackageName.add(userComponents.valueAt(j).getPackageName()); } + mEnabledServicesByUser.put(resolvedUserId, enabledServices); + mEnabledServicesPackageNamesByUser.put(resolvedUserId, enabledServicesPackageName); } } @@ -1453,13 +1583,9 @@ abstract public class ManagedServices { */ protected void rebindServices(boolean forceRebind, int userToRebind) { if (DEBUG) Slog.d(TAG, "rebindServices " + forceRebind + " " + userToRebind); - IntArray userIds = mUserProfiles.getCurrentProfileIds(); boolean rebindAllCurrentUsers = mUserProfiles.isProfileUser(userToRebind, mContext) && allowRebindForParentUser(); - if (userToRebind != USER_ALL && !rebindAllCurrentUsers) { - userIds = new IntArray(1); - userIds.add(userToRebind); - } + IntArray userIds = getUserIdsForRebindServices(userToRebind, rebindAllCurrentUsers); final SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>(); final SparseArray<Set<ComponentName>> componentsToUnbind = new SparseArray<>(); @@ -1483,6 +1609,23 @@ abstract public class ManagedServices { bindToServices(componentsToBind); } + private IntArray getUserIdsForRebindServices(int userToRebind, boolean rebindAllCurrentUsers) { + IntArray userIds = mUserProfiles.getCurrentProfileIds(); + if (userToRebind != USER_ALL && !rebindAllCurrentUsers) { + userIds = new IntArray(1); + userIds.add(userToRebind); + } else if (managedServicesConcurrentMultiuser() + && userToRebind == USER_ALL) { + for (UserInfo user : mUm.getUsers()) { + if (mUmInternal.isVisibleBackgroundFullUser(user.id) + && !userIds.contains(user.id)) { + userIds.add(user.id); + } + } + } + return userIds; + } + /** * Called when user switched to unbind all services from other users. */ @@ -1506,7 +1649,11 @@ abstract public class ManagedServices { synchronized (mMutex) { final Set<ManagedServiceInfo> removableBoundServices = getRemovableConnectedServices(); for (ManagedServiceInfo info : removableBoundServices) { - if ((allExceptUser && (info.userid != user)) + // User switching is the event for the forground user. + // It should not affect the service of the visible background user. + if ((allExceptUser && (info.userid != user) + && !(managedServicesConcurrentMultiuser() + && info.isVisibleBackgroundUserService)) || (!allExceptUser && (info.userid == user))) { Set<ComponentName> toUnbind = componentsToUnbind.get(info.userid, new ArraySet<>()); @@ -1861,6 +2008,29 @@ abstract public class ManagedServices { } /** + * This method returns the mapped id for the incoming user id + * If the incoming id was not the id of the visible background user, it returns USER_CURRENT. + * In the other cases, it returns the same value as the input. + * + * @param userId the id of the user + * @return the user id if it is a visible background user, otherwise + * {@link UserHandle#USER_CURRENT} + */ + @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + @VisibleForTesting + public int resolveUserId(int userId) { + if (managedServicesConcurrentMultiuser()) { + if (mUmInternal.isVisibleBackgroundFullUser(userId)) { + // The dataset of the visible background user should be managed independently. + return userId; + } + } + // The data of current user and its profile users need to be managed + // in a dataset as before. + return UserHandle.USER_CURRENT; + } + + /** * Returns true if services in the parent user should be rebound * when rebindServices is called with a profile userId. * Must be false for NotificationAssistants. @@ -1878,6 +2048,8 @@ abstract public class ManagedServices { public int targetSdkVersion; public Pair<ComponentName, Integer> mKey; public int uid; + @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public boolean isVisibleBackgroundUserService; public ManagedServiceInfo(IInterface service, ComponentName component, int userid, boolean isSystem, ServiceConnection connection, int targetSdkVersion, @@ -1889,6 +2061,10 @@ abstract public class ManagedServices { this.connection = connection; this.targetSdkVersion = targetSdkVersion; this.uid = uid; + if (managedServicesConcurrentMultiuser()) { + this.isVisibleBackgroundUserService = LocalServices + .getService(UserManagerInternal.class).isVisibleBackgroundFullUser(userid); + } mKey = Pair.create(component, userid); } @@ -1937,19 +2113,28 @@ abstract public class ManagedServices { } public boolean isSameUser(int userId) { - if (!isEnabledForCurrentProfiles()) { + if (!isEnabledForUser()) { return false; } return userId == USER_ALL || userId == this.userid; } public boolean enabledAndUserMatches(int nid) { - if (!isEnabledForCurrentProfiles()) { + if (!isEnabledForUser()) { return false; } if (this.userid == USER_ALL) return true; if (this.isSystem) return true; if (nid == USER_ALL || nid == this.userid) return true; + if (managedServicesConcurrentMultiuser() + && mUmInternal.getProfileParentId(nid) + != mUmInternal.getProfileParentId(this.userid)) { + // If the profile parent IDs do not match each other, + // it is determined that the users do not match. + // This situation may occur when comparing the current user's ID + // with the visible background user's ID. + return false; + } return supportsProfiles() && mUserProfiles.isCurrentProfile(nid) && isPermittedForProfile(nid); @@ -1969,12 +2154,21 @@ abstract public class ManagedServices { removeServiceImpl(this.service, this.userid); } - /** convenience method for looking in mEnabledServicesForCurrentProfiles */ - public boolean isEnabledForCurrentProfiles() { + /** + * convenience method for looking in mEnabledServicesByUser. + * If FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER is disabled, this manages the data using + * only UserHandle.USER_CURRENT as the key, in order to behave the same as the legacy logic. + */ + public boolean isEnabledForUser() { if (this.isSystem) return true; if (this.connection == null) return false; synchronized (mMutex) { - return mEnabledServicesForCurrentProfiles.contains(this.component); + int resolvedUserId = managedServicesConcurrentMultiuser() + ? resolveUserId(this.userid) + : UserHandle.USER_CURRENT; + ArraySet<ComponentName> enabledServices = + mEnabledServicesByUser.get(resolvedUserId); + return enabledServices != null && enabledServices.contains(this.component); } } @@ -2017,10 +2211,30 @@ abstract public class ManagedServices { } } - /** convenience method for looking in mEnabledServicesForCurrentProfiles */ + /** convenience method for looking in mEnabledServicesByUser for UserHandle.USER_CURRENT. + * This is a legacy API. When FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER becomes + * trunk stable, this API should be deprecated. Additionally, when this method + * is deprecated, the unit tests written using this method should also be revised. + * + * @param component target component name + * @return boolean value that indicates whether it is enabled for the current profiles + */ public boolean isComponentEnabledForCurrentProfiles(ComponentName component) { + return isComponentEnabledForUser(component, UserHandle.USER_CURRENT); + } + + /** convenience method for looking in mEnabledServicesForUser + * + * @param component target component name + * @param userId the id of the target user + * @return boolean value that indicates whether it is enabled for the target user + */ + @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public boolean isComponentEnabledForUser(ComponentName component, int userId) { synchronized (mMutex) { - return mEnabledServicesForCurrentProfiles.contains(component); + ArraySet<ComponentName> enabledServicesForUser = + mEnabledServicesByUser.get(resolveUserId(userId)); + return enabledServicesForUser != null && enabledServicesForUser.contains(component); } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 3a3deb00562e..09c8b5ba823e 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -173,6 +173,7 @@ import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; import static com.android.server.notification.Flags.expireBitmaps; +import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser; import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_ANIM_BUFFER; import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_TIMEOUT; import static com.android.server.utils.PriorityDump.PRIORITY_ARG; @@ -2323,6 +2324,9 @@ public class NotificationManagerService extends SystemService { if (userHandle >= 0) { cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, userHandle, REASON_USER_STOPPED); + mConditionProviders.onUserStopped(userHandle); + mListeners.onUserStopped(userHandle); + mAssistants.onUserStopped(userHandle); } } else if ( isProfileUnavailable(action)) { @@ -5244,6 +5248,21 @@ public class NotificationManagerService extends SystemService { } @Override + @FlaggedApi(android.app.Flags.FLAG_NM_BINDER_PERF_GET_APPS_WITH_CHANNELS) + public List<String> getPackagesWithAnyChannels(int userId) throws RemoteException { + checkCallerIsSystem(); + UserHandle user = UserHandle.of(userId); + List<String> packages = mPreferencesHelper.getPackagesWithAnyChannels(userId); + for (int i = packages.size() - 1; i >= 0; i--) { + String pkg = packages.get(i); + if (!areNotificationsEnabledForPackage(pkg, getUidForPackageAndUser(pkg, user))) { + packages.remove(i); + } + } + return packages; + } + + @Override public void clearData(String packageName, int uid, boolean fromApp) throws RemoteException { boolean packagesChanged = false; checkCallerIsSystem(); @@ -5715,12 +5734,13 @@ public class NotificationManagerService extends SystemService { public void requestBindListener(ComponentName component) { checkCallerIsSystemOrSameApp(component.getPackageName()); int uid = Binder.getCallingUid(); + int userId = UserHandle.getUserId(uid); final long identity = Binder.clearCallingIdentity(); try { - ManagedServices manager = - mAssistants.isComponentEnabledForCurrentProfiles(component) - ? mAssistants - : mListeners; + boolean isAssistantEnabled = managedServicesConcurrentMultiuser() + ? mAssistants.isComponentEnabledForUser(component, userId) + : mAssistants.isComponentEnabledForCurrentProfiles(component); + ManagedServices manager = isAssistantEnabled ? mAssistants : mListeners; manager.setComponentState(component, UserHandle.getUserId(uid), true); } finally { Binder.restoreCallingIdentity(identity); @@ -5747,16 +5767,16 @@ public class NotificationManagerService extends SystemService { public void requestUnbindListenerComponent(ComponentName component) { checkCallerIsSameApp(component.getPackageName()); int uid = Binder.getCallingUid(); + int userId = UserHandle.getUserId(uid); final long identity = Binder.clearCallingIdentity(); try { synchronized (mNotificationLock) { - ManagedServices manager = - mAssistants.isComponentEnabledForCurrentProfiles(component) - ? mAssistants - : mListeners; - if (manager.isPackageOrComponentAllowed(component.flattenToString(), - UserHandle.getUserId(uid))) { - manager.setComponentState(component, UserHandle.getUserId(uid), false); + boolean isAssistantEnabled = managedServicesConcurrentMultiuser() + ? mAssistants.isComponentEnabledForUser(component, userId) + : mAssistants.isComponentEnabledForCurrentProfiles(component); + ManagedServices manager = isAssistantEnabled ? mAssistants : mListeners; + if (manager.isPackageOrComponentAllowed(component.flattenToString(), userId)) { + manager.setComponentState(component, userId, false); } } } finally { @@ -6534,6 +6554,13 @@ public class NotificationManagerService extends SystemService { } catch (NameNotFoundException e) { return false; } + if (managedServicesConcurrentMultiuser()) { + return checkPackagePolicyAccess(pkg) + || mListeners.isComponentEnabledForPackage(pkg, + UserHandle.getCallingUserId()) + || (mDpm != null + && (mDpm.isActiveProfileOwner(uid) || mDpm.isActiveDeviceOwner(uid))); + } //TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode. return checkPackagePolicyAccess(pkg) || mListeners.isComponentEnabledForPackage(pkg) @@ -6938,7 +6965,8 @@ public class NotificationManagerService extends SystemService { android.Manifest.permission.INTERACT_ACROSS_USERS, "setNotificationListenerAccessGrantedForUser for user " + userId); } - if (mUmInternal.isVisibleBackgroundFullUser(userId)) { + if (!managedServicesConcurrentMultiuser() + && mUmInternal.isVisibleBackgroundFullUser(userId)) { // The main use case for visible background users is the Automotive multi-display // configuration where a passenger can use a secondary display while the driver is // using the main display. NotificationListeners is designed only for the current @@ -13150,7 +13178,8 @@ public class NotificationManagerService extends SystemService { @Override public void onUserUnlocked(int user) { - if (mUmInternal.isVisibleBackgroundFullUser(user)) { + if (!managedServicesConcurrentMultiuser() + && mUmInternal.isVisibleBackgroundFullUser(user)) { // The main use case for visible background users is the Automotive // multi-display configuration where a passenger can use a secondary // display while the driver is using the main display. @@ -13790,7 +13819,7 @@ public class NotificationManagerService extends SystemService { // TODO (b/73052211): if the ranking update changed the notification type, // cancel notifications for NLSes that can't see them anymore for (final ManagedServiceInfo serviceInfo : getServices()) { - if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener( + if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener( serviceInfo, ActivityManager.getCurrentUser())) { continue; } @@ -13818,7 +13847,7 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") public void notifyListenerHintsChangedLocked(final int hints) { for (final ManagedServiceInfo serviceInfo : getServices()) { - if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener( + if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener( serviceInfo, ActivityManager.getCurrentUser())) { continue; } @@ -13874,7 +13903,7 @@ public class NotificationManagerService extends SystemService { public void notifyInterruptionFilterChanged(final int interruptionFilter) { for (final ManagedServiceInfo serviceInfo : getServices()) { - if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener( + if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener( serviceInfo, ActivityManager.getCurrentUser())) { continue; } diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index a171ffc2ed98..3974c839fd38 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -2006,6 +2006,29 @@ public class PreferencesHelper implements RankingConfig { } /** + * Gets all apps for this user that have a nonzero number of channels. This count does not + * include deleted channels. + */ + @FlaggedApi(android.app.Flags.FLAG_NM_BINDER_PERF_GET_APPS_WITH_CHANNELS) + public @NonNull List<String> getPackagesWithAnyChannels(@UserIdInt int userId) { + List<String> pkgs = new ArrayList<>(); + synchronized (mLock) { + for (PackagePreferences p : mPackagePreferences.values()) { + if (UserHandle.getUserId(p.uid) != userId) { + continue; + } + for (NotificationChannel c : p.channels.values()) { + if (!c.isDeleted()) { + pkgs.add(p.pkg); + break; + } + } + } + } + return pkgs; + } + + /** * True for pre-O apps that only have the default channel, or pre O apps that have no * channels yet. This method will create the default channel for pre-O apps that don't have it. * Should never be true for O+ targeting apps, but that's enforced on boot/when an app diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 048f2b6b0cbc..76cd5c88b388 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -210,3 +210,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "managed_services_concurrent_multiuser" + namespace: "systemui" + description: "Enables ManagedServices to support Concurrent multi user environment" + bug: "380297485" +} diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 76c5240ab623..4153cd1be0a6 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -1163,6 +1163,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + private boolean shouldShowHub() { + final boolean hubEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), Settings.Secure.GLANCEABLE_HUB_ENABLED, + 1, mCurrentUserId) == 1; + + return mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled + && mDreamManagerInternal.dreamConditionActive(); + } + @VisibleForTesting void powerPress(long eventTime, int count, int displayId) { // SideFPS still needs to know about suppressed power buttons, in case it needs to block @@ -1261,9 +1270,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { // show hub. boolean keyguardAvailable = !mLockPatternUtils.isLockScreenDisabled( mCurrentUserId); - if (mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled - && keyguardAvailable && mDreamManagerInternal.dreamConditionActive()) { - // If the hub can be launched, send a message to keyguard. + if (shouldShowHub() && keyguardAvailable) { + // If the hub can be launched, send a message to keyguard. We do not know if + // the hub is already running or not, keyguard handles turning screen off if + // it is. Bundle options = new Bundle(); options.putBoolean(EXTRA_TRIGGER_HUB, true); lockNow(options); @@ -1324,14 +1334,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { * @param isScreenOn Whether the screen is currently on. * @param noDreamAction The action to perform if dreaming is not possible. */ - private void attemptToDreamFromShortPowerButtonPress( + private boolean attemptToDreamFromShortPowerButtonPress( boolean isScreenOn, Runnable noDreamAction) { if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) { // If the power button behavior isn't one that should be able to trigger the dream, give // up. noDreamAction.run(); - return; + return false; } final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal(); @@ -1339,7 +1349,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { Slog.d(TAG, "Can't start dreaming when attempting to dream from short power" + " press (isScreenOn=" + isScreenOn + ")"); noDreamAction.run(); - return; + return false; } synchronized (mLock) { @@ -1350,6 +1360,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } dreamManagerInternal.requestDream(); + + return true; } /** @@ -6398,6 +6410,17 @@ public class PhoneWindowManager implements WindowManagerPolicy { event.getDisplayId(), event.getKeyCode(), "wakeUpFromWakeKey")) { return; } + + if (!shouldShowHub() + && mShortPressOnPowerBehavior == SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP + && event.getKeyCode() == KEYCODE_POWER + && attemptToDreamFromShortPowerButtonPress(false, () -> {})) { + // In the case that we should wake to dream and successfully initiate dreaming, do not + // continue waking up. Doing so will exit the dream state and cause UI to react + // accordingly. + return; + } + wakeUpFromWakeKey( event.getEventTime(), event.getKeyCode(), diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java index 395816902592..d06827ab0529 100644 --- a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java +++ b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java @@ -273,6 +273,9 @@ final class WearableSensingManagerPerUserService @Override public void onError() { + synchronized (mLock) { + ensureRemoteServiceInitiated(); + } synchronized (mSecureChannelLock) { if (mSecureChannel != null && mSecureChannel diff --git a/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java b/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java index a16ff51e2d20..9f14ab7a70d3 100644 --- a/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java +++ b/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java @@ -156,6 +156,7 @@ final class WearableSensingSecureChannel { new AssociationRequest.Builder() .setDisplayName(CDM_ASSOCIATION_DISPLAY_NAME) .setSelfManaged(true) + .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING) .build(), mLightWeightExecutor, new CompanionDeviceManager.Callback() { @@ -195,7 +196,8 @@ final class WearableSensingSecureChannel { mCompanionDeviceManager.attachSystemDataTransport( associationId, new AutoCloseInputStream(mUnderlyingTransport), - new AutoCloseOutputStream(mUnderlyingTransport)); + new AutoCloseOutputStream(mUnderlyingTransport), + CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF); } } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml index b7de74987eb8..45523268c966 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml @@ -32,8 +32,8 @@ <uses-library android:name="android.test.runner" /> </application> - <!-- The "targetPackage" reference the instruments APK package, which is the FakeImeApk, while - the test package is "com.android.inputmethod.imetests" (FrameworksImeTests.apk).--> + <!-- The "targetPackage" reference the instruments APK package, which is the SimpleTestIme.apk, + while the test package is "com.android.inputmethod.imetests" (FrameworksImeTests.apk).--> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.android.apps.inputmethod.simpleime" diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index 72e9cc566497..5d64cb638702 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -92,6 +92,9 @@ public class InputMethodServiceTest { private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0"; + /** The ids of the subtypes of SimpleIme. */ + private static final int[] SUBTYPE_IDS = new int[]{1, 2}; + private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); private final GestureNavSwitchHelper mGestureNavSwitchHelper = new GestureNavSwitchHelper(); @@ -173,15 +176,14 @@ public class InputMethodServiceTest { Log.i(TAG, "Click on EditText"); verifyInputViewStatus( () -> clickOnViewCenter(mActivity.getEditText()), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Press home key to hide IME. Log.i(TAG, "Press home"); if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { assertWithMessage("Home key press was handled").that(mUiDevice.pressHome()).isTrue(); + // This doesn't call verifyInputViewStatus, as the refactor delays the events such that + // getCurrentInputStarted() would be false, as we would already be in launcher. // The IME visibility is only sent at the end of the animation. Therefore, we have to // wait until the visibility was sent to the server and the IME window hidden. eventually(() -> assertWithMessage("IME is not shown") @@ -190,11 +192,7 @@ public class InputMethodServiceTest { verifyInputViewStatus( () -> assertWithMessage("Home key press was handled") .that(mUiDevice.pressHome()).isTrue(), - EVENT_HIDE, - true /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } } @@ -208,26 +206,12 @@ public class InputMethodServiceTest { // Triggers to show IME via public API. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Triggers to hide IME via public API. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_HIDE, - true /* expected */, - false /* inputViewStarted */); - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } /** @@ -240,26 +224,12 @@ public class InputMethodServiceTest { // Triggers to show IME via public API. verifyInputViewStatusOnMainSync( () -> mActivity.showImeWithWindowInsetsController(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Triggers to hide IME via public API. verifyInputViewStatusOnMainSync( () -> mActivity.hideImeWithWindowInsetsController(), - EVENT_HIDE, - true /* expected */, - false /* inputViewStarted */); - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } /** @@ -277,10 +247,7 @@ public class InputMethodServiceTest { Log.i(TAG, "Call IMS#requestShowSelf(0)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestShowSelf(0 /* flags */), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown). @@ -288,51 +255,31 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestHideSelf( InputMethodManager.HIDE_IMPLICIT_ONLY), - EVENT_HIDE, - false /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown after HIDE_IMPLICIT_ONLY") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_IMPLICIT_ONLY"); } // IME request to hide itself without any flags, expect hidden. Log.i(TAG, "Call IMS#requestHideSelf(0)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestHideSelf(0 /* flags */), - EVENT_HIDE, - true /* expected */, - false /* inputViewStarted */); - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { // IME request to show itself with flag SHOW_IMPLICIT, expect shown. Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown with SHOW_IMPLICIT") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, + "IME is shown with SHOW_IMPLICIT"); // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden. Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestHideSelf( InputMethodManager.HIDE_IMPLICIT_ONLY), - EVENT_HIDE, - true /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown after HIDE_IMPLICIT_ONLY") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); } } @@ -424,20 +371,14 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager( InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, - false /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown after SHOW_IMPLICIT") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_SHOW, false /* eventExpected */, false /* shown */, + "IME is not shown after SHOW_IMPLICIT"); verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)) .isTrue(), - EVENT_SHOW, - false /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown after SHOW_EXPLICIT") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_SHOW, false /* eventExpected */, false /* shown */, + "IME is not shown after SHOW_EXPLICIT"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -455,10 +396,7 @@ public class InputMethodServiceTest { // IME should be shown. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); } /** @@ -472,10 +410,7 @@ public class InputMethodServiceTest { // the IME should be shown. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); } /** @@ -492,9 +427,9 @@ public class InputMethodServiceTest { .that(mUiDevice.isNaturalOrientation()).isFalse()); // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") - .that(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity)); + .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); // Get the new TestActivity. - mActivity = TestActivity.getLastCreatedInstance(); + mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. eventually(() -> assertWithMessage("Has an input connection to the re-created Activity") @@ -502,10 +437,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); } /** @@ -527,9 +459,9 @@ public class InputMethodServiceTest { .that(mUiDevice.isNaturalOrientation()).isFalse()); // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") - .that(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity)); + .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); // Get the new TestActivity. - mActivity = TestActivity.getLastCreatedInstance(); + mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. eventually(() -> assertWithMessage("Has an input connection to the re-created Activity") @@ -537,11 +469,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, - false /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); } /** @@ -561,10 +489,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -594,11 +519,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() ->assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)) .isTrue(), - EVENT_SHOW, - false /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -623,10 +544,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)) .isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Simulate connecting a hardware keyboard. config.keyboard = Configuration.KEYBOARD_QWERTY; @@ -637,11 +555,8 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync( () -> mInputMethodService.onConfigurationChanged(config), - EVENT_CONFIG, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown after a configuration change") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_CONFIG, true /* eventExpected */, true /* shown */, + "IME is still shown after a configuration change"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -672,10 +587,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager( InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Simulate connecting a hardware keyboard. config.keyboard = Configuration.KEYBOARD_QWERTY; @@ -692,11 +604,8 @@ public class InputMethodServiceTest { // still alive. verifyInputViewStatusOnMainSync( () -> mInputMethodService.onConfigurationChanged(config), - EVENT_CONFIG, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is not shown after a configuration change") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */, + false /* shown */, "IME is not shown after a configuration change"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -722,31 +631,21 @@ public class InputMethodServiceTest { // Explicit show request. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Implicit show request. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager( InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, - false /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, false /* eventExpected */, true /* shown */, "IME is still shown"); // Simulate a fake configuration change to avoid the recreation of TestActivity. // This should now consider the implicit show request, but keep the state from the // explicit show request, and thus not hide the IME. verifyInputViewStatusOnMainSync( () -> mInputMethodService.onConfigurationChanged(config), - EVENT_CONFIG, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown after a configuration change") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_CONFIG, true /* eventExpected */, true /* shown */, + "IME is still shown after a configuration change"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -769,27 +668,17 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - false /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, false /* eventExpected */, true /* shown */, "IME is still shown"); verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) .isTrue(), - EVENT_HIDE, - false /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown after HIDE_NOT_ALWAYS") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_NOT_ALWAYS"); } /** @@ -800,18 +689,15 @@ public class InputMethodServiceTest { setShowImeWithHardKeyboard(true /* enabled */); Log.i(TAG, "Set orientation natural"); - verifyFullscreenMode(() -> setOrientation(0), - false /* expected */, + verifyFullscreenMode(() -> setOrientation(0), false /* eventExpected */, true /* orientationPortrait */); Log.i(TAG, "Set orientation left"); - verifyFullscreenMode(() -> setOrientation(1), - true /* expected */, + verifyFullscreenMode(() -> setOrientation(1), true /* eventExpected */, false /* orientationPortrait */); Log.i(TAG, "Set orientation right"); - verifyFullscreenMode(() -> setOrientation(2), - false /* expected */, + verifyFullscreenMode(() -> setOrientation(2), false /* eventExpected */, false /* orientationPortrait */); } @@ -845,10 +731,7 @@ public class InputMethodServiceTest { setDrawsImeNavBarAndSwitcherButton(true /* enabled */); mActivity.showImeWithWindowInsetsController(); }, - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME navigation bar is initially shown") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue(); @@ -883,10 +766,7 @@ public class InputMethodServiceTest { setDrawsImeNavBarAndSwitcherButton(false /* enabled */); mActivity.showImeWithWindowInsetsController(); }, - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME navigation bar is initially not shown") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); @@ -917,24 +797,15 @@ public class InputMethodServiceTest { try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> mActivity.showImeWithWindowInsetsController(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); final var backButton = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); - backButton.click(); - mInstrumentation.waitForIdleSync(); - - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have - // to wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + verifyInputViewStatus( + () -> { + backButton.click(); + mInstrumentation.waitForIdleSync(); + }, + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } } @@ -952,30 +823,20 @@ public class InputMethodServiceTest { try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> mActivity.showImeWithWindowInsetsController(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); final var backButton = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); - backButton.longClick(); - mInstrumentation.waitForIdleSync(); - - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have - // to wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + verifyInputViewStatus( + () -> { + backButton.longClick(); + mInstrumentation.waitForIdleSync(); + }, + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } } /** - * Verifies that clicking on the IME switch button either shows the Input Method Switcher Menu, - * or switches the input method. + * Verifies that clicking on the IME switch button switches the input method subtype. */ @Test public void testImeSwitchButtonClick() throws Exception { @@ -985,34 +846,32 @@ public class InputMethodServiceTest { setShowImeWithHardKeyboard(true /* enabled */); + final var info = mImm.getCurrentInputMethodInfo(); + assertWithMessage("InputMethodInfo is not null").that(info).isNotNull(); + mImm.setExplicitlyEnabledInputMethodSubtypes(info.getId(), SUBTYPE_IDS); + + final var initialSubtype = mImm.getCurrentInputMethodSubtype(); + try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> { setDrawsImeNavBarAndSwitcherButton(true /* enabled */); mActivity.showImeWithWindowInsetsController(); }, - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); - - final var initialInfo = mImm.getCurrentInputMethodInfo(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); final var imeSwitcherButton = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); imeSwitcherButton.click(); mInstrumentation.waitForIdleSync(); - final var newInfo = mImm.getCurrentInputMethodInfo(); + final var newSubtype = mImm.getCurrentInputMethodSubtype(); - assertWithMessage("Input Method Switcher Menu is shown or input method was switched") - .that(isInputMethodPickerShown(mImm) || !Objects.equals(initialInfo, newInfo)) + assertWithMessage("Input method subtype was switched") + .that(!Objects.equals(initialSubtype, newSubtype)) .isTrue(); assertWithMessage("IME is still shown after IME Switcher button was clicked") .that(mInputMethodService.isInputViewShown()).isTrue(); - - // Hide the IME Switcher Menu before finishing. - mUiDevice.pressBack(); } } @@ -1027,16 +886,17 @@ public class InputMethodServiceTest { setShowImeWithHardKeyboard(true /* enabled */); + final var info = mImm.getCurrentInputMethodInfo(); + assertWithMessage("InputMethodInfo is not null").that(info).isNotNull(); + mImm.setExplicitlyEnabledInputMethodSubtypes(info.getId(), SUBTYPE_IDS); + try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> { setDrawsImeNavBarAndSwitcherButton(true /* enabled */); mActivity.showImeWithWindowInsetsController(); }, - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); final var imeSwitcherButton = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); imeSwitcherButton.longClick(); @@ -1052,58 +912,78 @@ public class InputMethodServiceTest { } } - private void verifyInputViewStatus(@NonNull Runnable runnable, @Event int event, - boolean expected, boolean inputViewStarted) { - verifyInputViewStatusInternal(runnable, event, expected, inputViewStarted, - false /* runOnMainSync */); + private void verifyInputViewStatus(@NonNull Runnable runnable, @Event int eventType, + boolean eventExpected, boolean shown, @NonNull String message) { + verifyInputViewStatusInternal(runnable, eventType, eventExpected, + shown /* inputViewStarted */, shown, message, false /* runOnMainSync */); + } + + private void verifyInputViewStatusOnMainSync(@NonNull Runnable runnable, @Event int eventType, + boolean eventExpected, boolean shown, @NonNull String message) { + verifyInputViewStatusInternal(runnable, eventType, eventExpected, + shown /* inputViewStarted */, shown, message, true /* runOnMainSync */); } - private void verifyInputViewStatusOnMainSync(@NonNull Runnable runnable, @Event int event, - boolean expected, boolean inputViewStarted) { - verifyInputViewStatusInternal(runnable, event, expected, inputViewStarted, - true /* runOnMainSync */); + private void verifyInputViewStatusOnMainSync(@NonNull Runnable runnable, @Event int eventType, + boolean eventExpected, boolean inputViewStarted, boolean shown, + @NonNull String message) { + verifyInputViewStatusInternal(runnable, eventType, eventExpected, inputViewStarted, shown, + message, true /* runOnMainSync */); } /** * Verifies the status of the Input View after executing the given runnable, and waiting that - * the event was either triggered or not, based on the given expectation. + * the event was either called or not. * - * @param runnable the runnable to trigger the event - * @param event the event to await. - * @param expected whether the event is expected to be triggered. - * @param inputViewStarted the expected state of the Input View after executing the runnable. + * @param runnable the runnable to call the event. + * @param eventType the event type to wait for. + * @param eventExpected whether the event is expected to be called. + * @param inputViewStarted whether the input view is expected to be started. + * @param shown whether the input view is expected to be shown. + * @param message the message for the input view shown assertion. * @param runOnMainSync whether to execute the runnable on the main thread. */ - private void verifyInputViewStatusInternal(@NonNull Runnable runnable, @Event int event, - boolean expected, boolean inputViewStarted, boolean runOnMainSync) { - final boolean completed; + private void verifyInputViewStatusInternal(@NonNull Runnable runnable, @Event int eventType, + boolean eventExpected, boolean inputViewStarted, boolean shown, @NonNull String message, + boolean runOnMainSync) { + final boolean eventCalled; try { final var latch = new CountDownLatch(1); - mInputMethodService.setCountDownLatchForTesting(latch, event); + mInputMethodService.setCountDownLatchForTesting(latch, eventType); if (runOnMainSync) { mInstrumentation.runOnMainSync(runnable); } else { runnable.run(); } mInstrumentation.waitForIdleSync(); - completed = latch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); + eventCalled = latch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException e) { fail("Interrupted while waiting for latch: " + e.getMessage()); return; } finally { - mInputMethodService.setCountDownLatchForTesting(null /* latch */, event); + mInputMethodService.setCountDownLatchForTesting(null /* latch */, eventType); } - if (expected && !completed) { - fail("Timed out waiting for " + eventToString(event)); - } else if (!expected && completed) { - fail("Unexpected call " + eventToString(event)); + if (eventExpected && !eventCalled) { + fail("Timed out waiting for " + eventToString(eventType)); + } else if (!eventExpected && eventCalled) { + fail("Unexpected call " + eventToString(eventType)); } - // Input is not finished. + // Input connection is not finished. assertWithMessage("Input connection is still started") .that(mInputMethodService.getCurrentInputStarted()).isTrue(); - assertWithMessage("IME visibility matches expected") + assertWithMessage("Input view started matches expected") .that(mInputMethodService.getCurrentInputViewStarted()).isEqualTo(inputViewStarted); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // The IME visibility is only sent at the end of the hide animation. Therefore, we have + // to wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertWithMessage(message).that(mInputMethodService.isInputViewShown()) + .isEqualTo(shown)); + } else { + assertWithMessage(message).that(mInputMethodService.isInputViewShown()) + .isEqualTo(shown); + } } private void setOrientation(int orientation) { @@ -1127,31 +1007,28 @@ public class InputMethodServiceTest { /** * Verifies the IME fullscreen mode state after executing the given runnable. * - * @param runnable the runnable to execute for setting the orientation. - * @param expected whether the runnable is expected to trigger the signal. + * @param runnable the runnable to set the orientation. + * @param eventExpected whether the event is expected to be called. * @param orientationPortrait whether the orientation is expected to be portrait. */ - private void verifyFullscreenMode(@NonNull Runnable runnable, boolean expected, + private void verifyFullscreenMode(@NonNull Runnable runnable, boolean eventExpected, boolean orientationPortrait) { - verifyInputViewStatus(runnable, EVENT_CONFIG, expected, false /* inputViewStarted */); - if (expected) { + verifyInputViewStatus(runnable, EVENT_CONFIG, eventExpected, false /* shown */, + "IME is not shown"); + if (eventExpected) { // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") - .that(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity)); + .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); // Get the new TestActivity. - mActivity = TestActivity.getLastCreatedInstance(); + mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. eventually(() -> assertWithMessage("Has an input connection to the re-created Activity") .that(mImm.hasActiveInputConnection(mActivity.getEditText())).isTrue()); } - verifyInputViewStatusOnMainSync( - () -> mActivity.showImeWithWindowInsetsController(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + verifyInputViewStatusOnMainSync(() -> mActivity.showImeWithWindowInsetsController(), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME orientation matches expected") .that(mInputMethodService.getResources().getConfiguration().orientation) @@ -1172,21 +1049,8 @@ public class InputMethodServiceTest { .that(mInputMethodService.isFullscreenMode()).isEqualTo(!orientationPortrait); // Hide IME before finishing the run. - verifyInputViewStatusOnMainSync( - () -> mActivity.hideImeWithWindowInsetsController(), - EVENT_HIDE, - true /* expected */, - false /* inputViewStarted */); - - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + verifyInputViewStatusOnMainSync(() -> mActivity.hideImeWithWindowInsetsController(), + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } private void prepareIme() { diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml index 872b0688814a..3dd4a97f464f 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml @@ -17,7 +17,14 @@ <input-method xmlns:android="http://schemas.android.com/apk/res/android"> <subtype - android:label="FakeIme" + android:label="SimpleIme" android:imeSubtypeLocale="en_US" - android:imeSubtypeMode="keyboard"/> + android:imeSubtypeMode="keyboard" + android:subtypeId="1" /> + + <subtype + android:label="SimpleIme French" + android:imeSubtypeLocale="fr_FR" + android:imeSubtypeMode="keyboard" + android:subtypeId="2" /> </input-method>
\ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboardView.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboardView.java index 55d1b451c6b0..1840cdea4c20 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboardView.java +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboardView.java @@ -23,6 +23,7 @@ import android.util.Log; import android.util.SparseArray; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.WindowInsets; import android.widget.FrameLayout; import android.widget.TextView; @@ -107,6 +108,15 @@ final class SimpleKeyboardView extends FrameLayout { mapSoftKeys(); } + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + // Handle edge to edge for navigationBars insets (system nav bar) + // and captionBars insets (IME navigation bar). + final int insetBottom = insets.getInsets(WindowInsets.Type.systemBars()).bottom; + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), insetBottom); + return insets.inset(0, 0, 0, insetBottom); + } + /** * Sets the key press listener. * diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java index 3a7abbb167ec..558d1a7c4e8b 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java @@ -27,6 +27,7 @@ import androidx.annotation.Nullable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; import java.util.concurrent.CountDownLatch; /** Wrapper of {@link InputMethodService} to expose interfaces for testing purpose. */ @@ -35,8 +36,8 @@ public class InputMethodServiceWrapper extends InputMethodService { private static final String TAG = "InputMethodServiceWrapper"; /** Last created instance of this wrapper. */ - @Nullable - private static InputMethodServiceWrapper sInstance; + @NonNull + private static WeakReference<InputMethodServiceWrapper> sInstance = new WeakReference<>(null); /** IME show event ({@link #onStartInputView}). */ public static final int EVENT_SHOW = 0; @@ -68,32 +69,11 @@ public class InputMethodServiceWrapper extends InputMethodService { @Nullable private CountDownLatch mCountDownLatch; - /** Gets the last created instance of this wrapper, if available. */ - @Nullable - public static InputMethodServiceWrapper getInstance() { - return sInstance; - } - - public boolean getCurrentInputViewStarted() { - return mInputViewStarted; - } - - /** - * Sets the latch used to wait for the IME event. - * - * @param latch the latch to wait on. - * @param latchEvent the event to set the latch on. - */ - public void setCountDownLatchForTesting(@Nullable CountDownLatch latch, @Event int latchEvent) { - mCountDownLatch = latch; - mLatchEvent = latchEvent; - } - @Override public void onCreate() { Log.i(TAG, "onCreate()"); super.onCreate(); - sInstance = this; + sInstance = new WeakReference<>(this); } @Override @@ -103,6 +83,12 @@ public class InputMethodServiceWrapper extends InputMethodService { } @Override + public void onFinishInput() { + Log.i(TAG, "onFinishInput()"); + super.onFinishInput(); + } + + @Override public void onStartInputView(EditorInfo info, boolean restarting) { Log.i(TAG, "onStartInputView() editor=" + dumpEditorInfo(info) + ", restarting=" + restarting); @@ -114,12 +100,6 @@ public class InputMethodServiceWrapper extends InputMethodService { } @Override - public void onFinishInput() { - Log.i(TAG, "onFinishInput()"); - super.onFinishInput(); - } - - @Override public void onFinishInputView(boolean finishingInput) { Log.i(TAG, "onFinishInputView()"); super.onFinishInputView(finishingInput); @@ -146,14 +126,35 @@ public class InputMethodServiceWrapper extends InputMethodService { } } + public boolean getCurrentInputViewStarted() { + return mInputViewStarted; + } + + /** + * Sets the latch used to wait for the IME event. + * + * @param latch the latch to wait on. + * @param latchEvent the event to set the latch on. + */ + public void setCountDownLatchForTesting(@Nullable CountDownLatch latch, @Event int latchEvent) { + mCountDownLatch = latch; + mLatchEvent = latchEvent; + } + + /** Gets the last created instance of this wrapper, if available. */ + @Nullable + public static InputMethodServiceWrapper getInstance() { + return sInstance.get(); + } + /** * Gets the string representation of the IME event that is being waited on. * - * @param event the IME event. + * @param eventType the IME event type. */ @NonNull - public static String eventToString(@Event int event) { - return switch (event) { + public static String eventToString(@Event int eventType) { + return switch (eventType) { case EVENT_SHOW -> "onStartInputView"; case EVENT_HIDE -> "onFinishInputView"; case EVENT_CONFIG -> "onConfigurationChanged"; diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java index eadbac3ca74b..ad90b32848b0 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java @@ -45,30 +45,13 @@ import java.lang.ref.WeakReference; public final class TestActivity extends Activity { private static final String TAG = "TestActivity"; - private static WeakReference<TestActivity> sLastCreatedInstance = new WeakReference<>(null); - /** - * Start a new test activity with an editor and wait for it to begin running before returning. - * - * @param instrumentation application instrumentation - * @return the newly started activity - */ + /** Last created instance of this activity. */ @NonNull - public static TestActivity startSync(@NonNull Instrumentation instrumentation) { - final var intent = new Intent() - .setAction(Intent.ACTION_MAIN) - .setClass(instrumentation.getTargetContext(), TestActivity.class) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - return (TestActivity) instrumentation.startActivitySync(intent); - } + private static WeakReference<TestActivity> sInstance = new WeakReference<>(null); private EditText mEditText; - public EditText getEditText() { - return mEditText; - } - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -80,13 +63,11 @@ public final class TestActivity extends Activity { rootView.setFitsSystemWindows(true); setContentView(rootView); mEditText.requestFocus(); - sLastCreatedInstance = new WeakReference<>(this); + sInstance = new WeakReference<>(this); } - /** Get the last created TestActivity instance, if available. */ - @Nullable - public static TestActivity getLastCreatedInstance() { - return sLastCreatedInstance.get(); + public EditText getEditText() { + return mEditText; } /** Shows soft keyboard via InputMethodManager. */ @@ -118,4 +99,26 @@ public final class TestActivity extends Activity { controller.hide(WindowInsets.Type.ime()); Log.i(TAG, "hideIme() via WindowInsetsController"); } + + /** Gets the last created instance of this activity, if available. */ + @Nullable + public static TestActivity getInstance() { + return sInstance.get(); + } + + /** + * Start a new test activity with an editor and wait for it to begin running before returning. + * + * @param instrumentation application instrumentation. + * @return the newly started activity. + */ + @NonNull + public static TestActivity startSync(@NonNull Instrumentation instrumentation) { + final var intent = new Intent() + .setAction(Intent.ACTION_MAIN) + .setClass(instrumentation.getTargetContext(), TestActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + return (TestActivity) instrumentation.startActivitySync(intent); + } } diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 0eb20eb22380..66d7611a29c6 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -32,6 +32,7 @@ android_test { "androidx.test.rules", "hamcrest-library", "mockito-target-inline-minus-junit4", + "mockito-target-extended", "platform-compat-test-rules", "platform-test-annotations", "platformprotosnano", diff --git a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java index b3ec2153542a..c9d5241c57b7 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java +++ b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java @@ -30,6 +30,7 @@ import android.testing.TestableContext; import androidx.test.InstrumentationRegistry; +import com.android.server.pm.UserManagerInternal; import com.android.server.uri.UriGrantsManagerInternal; import org.junit.After; @@ -41,6 +42,7 @@ import org.mockito.MockitoAnnotations; public class UiServiceTestCase { @Mock protected PackageManagerInternal mPmi; + @Mock protected UserManagerInternal mUmi; @Mock protected UriGrantsManagerInternal mUgmInternal; protected static final String PKG_N_MR1 = "com.example.n_mr1"; @@ -92,6 +94,8 @@ public class UiServiceTestCase { } }); + LocalServices.removeServiceForTest(UserManagerInternal.class); + LocalServices.addService(UserManagerInternal.class, mUmi); LocalServices.removeServiceForTest(UriGrantsManagerInternal.class); LocalServices.addService(UriGrantsManagerInternal.class, mUgmInternal); when(mUgmInternal.checkGrantUriPermission( diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index e5c42082ab97..98440ecdad82 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -17,12 +17,17 @@ package com.android.server.notification; import static android.content.Context.DEVICE_POLICY_SERVICE; import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR; +import static android.os.UserHandle.USER_ALL; +import static android.os.UserHandle.USER_CURRENT; import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER; +import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser; import static com.android.server.notification.ManagedServices.APPROVAL_BY_COMPONENT; import static com.android.server.notification.ManagedServices.APPROVAL_BY_PACKAGE; import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled; @@ -66,7 +71,9 @@ import android.os.IInterface; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; @@ -83,6 +90,7 @@ import com.android.server.UiServiceTestCase; import com.google.android.collect.Lists; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -105,6 +113,9 @@ import java.util.concurrent.CountDownLatch; public class ManagedServicesTest extends UiServiceTestCase { + @Rule + public SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private IPackageManager mIpm; @Mock @@ -155,6 +166,7 @@ public class ManagedServicesTest extends UiServiceTestCase { users.add(new UserInfo(11, "11", 0)); users.add(new UserInfo(12, "12", 0)); users.add(new UserInfo(13, "13", 0)); + users.add(new UserInfo(99, "99", 0)); for (UserInfo user : users) { when(mUm.getUserInfo(eq(user.id))).thenReturn(user); } @@ -804,6 +816,7 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void rebindServices_onlyBindsExactMatchesIfComponent() throws Exception { // If the primary and secondary lists contain component names, only those components within // the package should be matched @@ -841,6 +854,45 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void rebindServices_onlyBindsExactMatchesIfComponent_concurrent_multiUser() + throws Exception { + // If the primary and secondary lists contain component names, only those components within + // the package should be matched + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, + mIpm, + ManagedServices.APPROVAL_BY_COMPONENT); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + packages.add("anotherPackage"); + addExpectedServices(service, packages, 0); + + // only 2 components are approved per package + mExpectedPrimaryComponentNames.clear(); + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2"); + mExpectedSecondaryComponentNames.clear(); + mExpectedSecondaryComponentNames.put(0, "anotherPackage/C1:anotherPackage/C2"); + + loadXml(service); + // verify the 2 components per package are enabled (bound) + verifyExpectedBoundEntries(service, true, 0); + verifyExpectedBoundEntries(service, false, 0); + + // verify the last component per package is not enabled/we don't try to bind to it + for (String pkg : packages) { + ComponentName unapprovedAdditionalComponent = + ComponentName.unflattenFromString(pkg + "/C3"); + assertFalse( + service.isComponentEnabledForUser( + unapprovedAdditionalComponent, 0)); + verify(mIpm, never()).getServiceInfo( + eq(unapprovedAdditionalComponent), anyLong(), anyInt()); + } + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void rebindServices_bindsEverythingInAPackage() throws Exception { // If the primary and secondary lists contain packages, all components within those packages // should be bound @@ -866,6 +918,32 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void rebindServices_bindsEverythingInAPackage_concurrent_multiUser() throws Exception { + // If the primary and secondary lists contain packages, all components within those packages + // should be bound + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_PACKAGE); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + packages.add("packagea"); + addExpectedServices(service, packages, 0); + + // 2 approved packages + mExpectedPrimaryPackages.clear(); + mExpectedPrimaryPackages.put(0, "package"); + mExpectedSecondaryPackages.clear(); + mExpectedSecondaryPackages.put(0, "packagea"); + + loadXml(service); + + // verify the 3 components per package are enabled (bound) + verifyExpectedBoundEntries(service, true, 0); + verifyExpectedBoundEntries(service, false, 0); + } + + @Test public void reregisterService_checksAppIsApproved_pkg() throws Exception { Context context = mock(Context.class); PackageManager pm = mock(PackageManager.class); @@ -1118,6 +1196,7 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void testUpgradeAppBindsNewServices() throws Exception { // If the primary and secondary lists contain component names, only those components within // the package should be matched @@ -1159,6 +1238,49 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUpgradeAppBindsNewServices_concurrent_multiUser() throws Exception { + // If the primary and secondary lists contain component names, only those components within + // the package should be matched + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, + mIpm, + ManagedServices.APPROVAL_BY_PACKAGE); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + addExpectedServices(service, packages, 0); + + // only 2 components are approved per package + mExpectedPrimaryComponentNames.clear(); + mExpectedPrimaryPackages.clear(); + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2"); + mExpectedSecondaryComponentNames.clear(); + mExpectedSecondaryPackages.clear(); + + loadXml(service); + + // new component expected + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2:package/C3"); + + service.onPackagesChanged(false, new String[]{"package"}, new int[]{0}); + + // verify the 3 components per package are enabled (bound) + verifyExpectedBoundEntries(service, true, 0); + + // verify the last component per package is not enabled/we don't try to bind to it + for (String pkg : packages) { + ComponentName unapprovedAdditionalComponent = + ComponentName.unflattenFromString(pkg + "/C3"); + assertFalse( + service.isComponentEnabledForUser( + unapprovedAdditionalComponent, 0)); + verify(mIpm, never()).getServiceInfo( + eq(unapprovedAdditionalComponent), anyLong(), anyInt()); + } + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void testUpgradeAppNoPermissionNoRebind() throws Exception { Context context = spy(getContext()); doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any()); @@ -1211,6 +1333,59 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUpgradeAppNoPermissionNoRebind_concurrent_multiUser() throws Exception { + Context context = spy(getContext()); + doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any()); + + ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, + mIpm, + APPROVAL_BY_COMPONENT); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + addExpectedServices(service, packages, 0); + + final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1"); + final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2"); + + // Both components are approved initially + mExpectedPrimaryComponentNames.clear(); + mExpectedPrimaryPackages.clear(); + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2"); + mExpectedSecondaryComponentNames.clear(); + mExpectedSecondaryPackages.clear(); + + loadXml(service); + + //Component package/C1 loses bind permission + when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer( + (Answer<ServiceInfo>) invocation -> { + ComponentName invocationCn = invocation.getArgument(0); + if (invocationCn != null) { + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = invocationCn.getPackageName(); + serviceInfo.name = invocationCn.getClassName(); + if (invocationCn.equals(unapprovedComponent)) { + serviceInfo.permission = "none"; + } else { + serviceInfo.permission = service.getConfig().bindPermission; + } + serviceInfo.metaData = null; + return serviceInfo; + } + return null; + } + ); + + // Trigger package update + service.onPackagesChanged(false, new String[]{"package"}, new int[]{0}); + + assertFalse(service.isComponentEnabledForUser(unapprovedComponent, 0)); + assertTrue(service.isComponentEnabledForUser(approvedComponent, 0)); + } + + @Test public void testSetPackageOrComponentEnabled() throws Exception { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, @@ -1517,6 +1692,201 @@ public class ManagedServicesTest extends UiServiceTestCase { assertTrue(componentsToBind.get(10).contains(ComponentName.unflattenFromString("c/c"))); } + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testPopulateComponentsToBindWithNonProfileUser() { + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + spyOn(service); + + SparseArray<ArraySet<ComponentName>> approvedComponentsByUser = new SparseArray<>(); + ArraySet<ComponentName> allowed0 = new ArraySet<>(); + allowed0.add(ComponentName.unflattenFromString("a/a")); + approvedComponentsByUser.put(0, allowed0); + ArraySet<ComponentName> allowed10 = new ArraySet<>(); + allowed10.add(ComponentName.unflattenFromString("b/b")); + approvedComponentsByUser.put(10, allowed10); + + int nonProfileUser = 99; + ArraySet<ComponentName> allowedForNonProfileUser = new ArraySet<>(); + allowedForNonProfileUser.add(ComponentName.unflattenFromString("c/c")); + approvedComponentsByUser.put(nonProfileUser, allowedForNonProfileUser); + + IntArray users = new IntArray(); + users.add(nonProfileUser); + users.add(10); + users.add(0); + + SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>(); + spyOn(service.mUmInternal); + when(service.mUmInternal.isVisibleBackgroundFullUser(nonProfileUser)).thenReturn(true); + + service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser); + + assertTrue(service.isComponentEnabledForUser( + ComponentName.unflattenFromString("a/a"), 0)); + assertTrue(service.isComponentEnabledForPackage("a", 0)); + assertTrue(service.isComponentEnabledForUser( + ComponentName.unflattenFromString("b/b"), 10)); + assertTrue(service.isComponentEnabledForPackage("b", 0)); + assertTrue(service.isComponentEnabledForPackage("b", 10)); + assertTrue(service.isComponentEnabledForUser( + ComponentName.unflattenFromString("c/c"), nonProfileUser)); + assertTrue(service.isComponentEnabledForPackage("c", nonProfileUser)); + } + + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testRebindService_profileUser() throws Exception { + final int profileUserId = 10; + when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); + spyOn(mService); + ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass( + IntArray.class); + when(mService.allowRebindForParentUser()).thenReturn(true); + + mService.rebindServices(false, profileUserId); + + verify(mService).populateComponentsToBind(any(), captor.capture(), any()); + assertTrue(captor.getValue().contains(0)); + assertTrue(captor.getValue().contains(profileUserId)); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testRebindService_nonProfileUser() throws Exception { + final int userId = 99; + when(mUserProfiles.isProfileUser(userId, mContext)).thenReturn(false); + spyOn(mService); + ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass( + IntArray.class); + when(mService.allowRebindForParentUser()).thenReturn(true); + + mService.rebindServices(false, userId); + + verify(mService).populateComponentsToBind(any(), captor.capture(), any()); + assertFalse(captor.getValue().contains(0)); + assertTrue(captor.getValue().contains(userId)); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testRebindService_userAll() throws Exception { + final int userId = 99; + spyOn(mService); + spyOn(mService.mUmInternal); + when(mService.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true); + ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass( + IntArray.class); + when(mService.allowRebindForParentUser()).thenReturn(true); + + mService.rebindServices(false, USER_ALL); + + verify(mService).populateComponentsToBind(any(), captor.capture(), any()); + assertTrue(captor.getValue().contains(0)); + assertTrue(captor.getValue().contains(userId)); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testOnUserStoppedWithVisibleBackgroundUser() throws Exception { + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + spyOn(service); + int userId = 99; + SparseArray<ArraySet<ComponentName>> approvedComponentsByUser = new SparseArray<>(); + ArraySet<ComponentName> allowedForNonProfileUser = new ArraySet<>(); + allowedForNonProfileUser.add(ComponentName.unflattenFromString("a/a")); + approvedComponentsByUser.put(userId, allowedForNonProfileUser); + IntArray users = new IntArray(); + users.add(userId); + SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>(); + spyOn(service.mUmInternal); + when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true); + service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser); + assertTrue(service.isComponentEnabledForUser( + ComponentName.unflattenFromString("a/a"), userId)); + assertTrue(service.isComponentEnabledForPackage("a", userId)); + + service.onUserStopped(userId); + + assertFalse(service.isComponentEnabledForUser( + ComponentName.unflattenFromString("a/a"), userId)); + assertFalse(service.isComponentEnabledForPackage("a", userId)); + verify(service).unbindUserServices(eq(userId)); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUnbindServicesImpl_serviceOfForegroundUser() throws Exception { + int switchingUserId = 10; + int userId = 99; + + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + spyOn(service); + spyOn(service.mUmInternal); + when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(false); + + IInterface iInterface = mock(IInterface.class); + when(iInterface.asBinder()).thenReturn(mock(IBinder.class)); + + ManagedServices.ManagedServiceInfo serviceInfo = service.new ManagedServiceInfo( + iInterface, ComponentName.unflattenFromString("a/a"), userId, false, + mock(ServiceConnection.class), 26, 34); + + Set<ManagedServices.ManagedServiceInfo> removableBoundServices = new ArraySet<>(); + removableBoundServices.add(serviceInfo); + + when(service.getRemovableConnectedServices()).thenReturn(removableBoundServices); + ArgumentCaptor<SparseArray<Set<ComponentName>>> captor = ArgumentCaptor.forClass( + SparseArray.class); + + service.unbindServicesImpl(switchingUserId, true); + + verify(service).unbindFromServices(captor.capture()); + + assertEquals(captor.getValue().size(), 1); + assertTrue(captor.getValue().indexOfKey(userId) != -1); + assertTrue(captor.getValue().get(userId).contains( + ComponentName.unflattenFromString("a/a"))); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUnbindServicesImpl_serviceOfVisibleBackgroundUser() throws Exception { + int switchingUserId = 10; + int userId = 99; + + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + spyOn(service); + spyOn(service.mUmInternal); + when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true); + + IInterface iInterface = mock(IInterface.class); + when(iInterface.asBinder()).thenReturn(mock(IBinder.class)); + + ManagedServices.ManagedServiceInfo serviceInfo = service.new ManagedServiceInfo( + iInterface, ComponentName.unflattenFromString("a/a"), userId, + false, mock(ServiceConnection.class), 26, 34); + + Set<ManagedServices.ManagedServiceInfo> removableBoundServices = new ArraySet<>(); + removableBoundServices.add(serviceInfo); + + when(service.getRemovableConnectedServices()).thenReturn(removableBoundServices); + ArgumentCaptor<SparseArray<Set<ComponentName>>> captor = ArgumentCaptor.forClass( + SparseArray.class); + + service.unbindServicesImpl(switchingUserId, true); + + verify(service).unbindFromServices(captor.capture()); + + assertEquals(captor.getValue().size(), 0); + } + @Test public void testOnNullBinding() throws Exception { Context context = mock(Context.class); @@ -1681,6 +2051,7 @@ public class ManagedServicesTest extends UiServiceTestCase { assertFalse(service.isBound(cn, mZero.id)); assertFalse(service.isBound(cn, mTen.id)); } + @Test public void testOnPackagesChanged_nullValuesPassed_noNullPointers() { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { @@ -2012,6 +2383,7 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isComponentEnabledForCurrentProfiles_isThreadSafe() throws InterruptedException { for (UserInfo userInfo : mUm.getUsers()) { mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", userInfo.id, true); @@ -2024,6 +2396,20 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isComponentEnabledForUser_isThreadSafe() throws InterruptedException { + for (UserInfo userInfo : mUm.getUsers()) { + mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", userInfo.id, true); + } + testThreadSafety(() -> { + mService.rebindServices(false, 0); + assertThat(mService.isComponentEnabledForUser( + new ComponentName("pkg1", "cmp1"), 0)).isTrue(); + }, 20, 30); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isComponentEnabledForCurrentProfiles_profileUserId() { final int profileUserId = 10; when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); @@ -2037,6 +2423,24 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isComponentEnabledForUser_profileUserId() { + final int profileUserId = 10; + when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); + spyOn(mService); + doReturn(USER_CURRENT).when(mService).resolveUserId(anyInt()); + + // Only approve for parent user (0) + mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", 0, true); + + // Test that the component is enabled after calling rebindServices with profile userId (10) + mService.rebindServices(false, profileUserId); + assertThat(mService.isComponentEnabledForUser( + new ComponentName("pkg1", "cmp1"), profileUserId)).isTrue(); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isComponentEnabledForCurrentProfiles_profileUserId_NAS() { final int profileUserId = 10; when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); @@ -2054,6 +2458,25 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isComponentEnabledForUser_profileUserId_NAS() { + final int profileUserId = 10; + when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); + // Do not rebind for parent users (NAS use-case) + ManagedServices service = spy(mService); + when(service.allowRebindForParentUser()).thenReturn(false); + doReturn(USER_CURRENT).when(service).resolveUserId(anyInt()); + + // Only approve for parent user (0) + service.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", 0, true); + + // Test that the component is disabled after calling rebindServices with profile userId (10) + service.rebindServices(false, profileUserId); + assertThat(service.isComponentEnabledForUser( + new ComponentName("pkg1", "cmp1"), profileUserId)).isFalse(); + } + + @Test @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) public void testManagedServiceInfoIsSystemUi() { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, @@ -2069,6 +2492,48 @@ public class ManagedServicesTest extends UiServiceTestCase { assertThat(service0.isSystemUi()).isFalse(); } + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUserMatchesAndEnabled_profileUser() throws Exception { + int currentUserId = 10; + int profileUserId = 11; + + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + ManagedServices.ManagedServiceInfo listener = spy(service.new ManagedServiceInfo( + mock(IInterface.class), ComponentName.unflattenFromString("a/a"), currentUserId, + false, mock(ServiceConnection.class), 26, 34)); + + doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(profileUserId); + doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(currentUserId); + doReturn(true).when(listener).isEnabledForUser(); + doReturn(true).when(mUserProfiles).isCurrentProfile(anyInt()); + + assertThat(listener.enabledAndUserMatches(profileUserId)).isTrue(); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUserMatchesAndDisabled_visibleBackgroudUser() throws Exception { + int currentUserId = 10; + int profileUserId = 11; + int visibleBackgroundUserId = 12; + + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + ManagedServices.ManagedServiceInfo listener = spy(service.new ManagedServiceInfo( + mock(IInterface.class), ComponentName.unflattenFromString("a/a"), profileUserId, + false, mock(ServiceConnection.class), 26, 34)); + + doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(profileUserId); + doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(currentUserId); + doReturn(visibleBackgroundUserId).when(service.mUmInternal) + .getProfileParentId(visibleBackgroundUserId); + doReturn(true).when(listener).isEnabledForUser(); + + assertThat(listener.enabledAndUserMatches(visibleBackgroundUserId)).isFalse(); + } + private void mockServiceInfoWithMetaData(List<ComponentName> componentNames, ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas) throws RemoteException { @@ -2247,26 +2712,47 @@ public class ManagedServicesTest extends UiServiceTestCase { private void verifyExpectedBoundEntries(ManagedServices service, boolean primary) throws Exception { + verifyExpectedBoundEntries(service, primary, UserHandle.USER_CURRENT); + } + + private void verifyExpectedBoundEntries(ManagedServices service, boolean primary, + int targetUserId) throws Exception { ArrayMap<Integer, String> verifyMap = primary ? mExpectedPrimary.get(service.mApprovalLevel) : mExpectedSecondary.get(service.mApprovalLevel); for (int userId : verifyMap.keySet()) { for (String packageOrComponent : verifyMap.get(userId).split(":")) { if (!TextUtils.isEmpty(packageOrComponent)) { if (service.mApprovalLevel == APPROVAL_BY_PACKAGE) { - assertTrue(packageOrComponent, - service.isComponentEnabledForPackage(packageOrComponent)); + if (managedServicesConcurrentMultiuser()) { + assertTrue(packageOrComponent, + service.isComponentEnabledForPackage(packageOrComponent, + targetUserId)); + } else { + assertTrue(packageOrComponent, + service.isComponentEnabledForPackage(packageOrComponent)); + } for (int i = 1; i <= 3; i++) { ComponentName componentName = ComponentName.unflattenFromString( packageOrComponent +"/C" + i); - assertTrue(service.isComponentEnabledForCurrentProfiles( - componentName)); + if (managedServicesConcurrentMultiuser()) { + assertTrue(service.isComponentEnabledForUser( + componentName, targetUserId)); + } else { + assertTrue(service.isComponentEnabledForCurrentProfiles( + componentName)); + } verify(mIpm, times(1)).getServiceInfo( eq(componentName), anyLong(), anyInt()); } } else { ComponentName componentName = ComponentName.unflattenFromString(packageOrComponent); - assertTrue(service.isComponentEnabledForCurrentProfiles(componentName)); + if (managedServicesConcurrentMultiuser()) { + assertTrue(service.isComponentEnabledForUser(componentName, + targetUserId)); + } else { + assertTrue(service.isComponentEnabledForCurrentProfiles(componentName)); + } verify(mIpm, times(1)).getServiceInfo( eq(componentName), anyLong(), anyInt()); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 0373eb6e9318..858dd3a605d8 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -140,6 +140,7 @@ import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL; +import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER; import static com.android.server.notification.Flags.FLAG_REJECT_OLD_NOTIFICATIONS; import static com.android.server.notification.GroupHelper.AUTOGROUP_KEY; import static com.android.server.notification.NotificationManagerService.BITMAP_DURATION; @@ -867,7 +868,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { && filter.hasAction(Intent.ACTION_PACKAGES_SUSPENDED)) { mPackageIntentReceiver = broadcastReceivers.get(i); } - if (filter.hasAction(Intent.ACTION_USER_SWITCHED) + if (filter.hasAction(Intent.ACTION_USER_STOPPED) + || filter.hasAction(Intent.ACTION_USER_SWITCHED) || filter.hasAction(Intent.ACTION_PROFILE_UNAVAILABLE) || filter.hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) { // There may be multiple receivers, get the NMS one @@ -5383,6 +5385,42 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testGetPackagesWithChannels_blocked() throws Exception { + // While we mostly rely on the PreferencesHelper implementation of channels, we filter in + // NMS so that we do not return blocked packages. + // Three packages; all under user 1. + // pkg2 is blocked, but pkg1 and pkg3 are not. + String pkg1 = "com.package.one", pkg2 = "com.package.two", pkg3 = "com.package.three"; + int uid1 = UserHandle.getUid(1, 111); + int uid2 = UserHandle.getUid(1, 222); + int uid3 = UserHandle.getUid(1, 333); + + when(mPackageManager.getPackageUid(eq(pkg1), anyLong(), anyInt())).thenReturn(uid1); + when(mPackageManager.getPackageUid(eq(pkg2), anyLong(), anyInt())).thenReturn(uid2); + when(mPackageManager.getPackageUid(eq(pkg3), anyLong(), anyInt())).thenReturn(uid3); + when(mPermissionHelper.hasPermission(uid1)).thenReturn(true); + when(mPermissionHelper.hasPermission(uid2)).thenReturn(false); + when(mPermissionHelper.hasPermission(uid3)).thenReturn(true); + + NotificationChannel channel1 = new NotificationChannel("id1", "name1", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel channel2 = new NotificationChannel("id3", "name3", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel channel3 = new NotificationChannel("id4", "name3", + NotificationManager.IMPORTANCE_DEFAULT); + mService.mPreferencesHelper.createNotificationChannel(pkg1, uid1, channel1, true, false, + uid1, false); + mService.mPreferencesHelper.createNotificationChannel(pkg2, uid2, channel2, true, false, + uid2, false); + mService.mPreferencesHelper.createNotificationChannel(pkg3, uid3, channel3, true, false, + uid3, false); + + // Output should contain only the package with notification permissions (1, 3). + enableInteractAcrossUsers(); + assertThat(mBinderService.getPackagesWithAnyChannels(1)).containsExactly(pkg1, pkg3); + } + + @Test public void testHasCompanionDevice_failure() throws Exception { when(mCompanionMgr.getAssociations(anyString(), anyInt())).thenThrow( new IllegalArgumentException()); @@ -16287,6 +16325,20 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void onUserStopped_callBackToListeners() { + Intent intent = new Intent(Intent.ACTION_USER_STOPPED); + intent.putExtra(Intent.EXTRA_USER_HANDLE, 20); + + mUserIntentReceiver.onReceive(mContext, intent); + + verify(mConditionProviders).onUserStopped(eq(20)); + verify(mListeners).onUserStopped(eq(20)); + verify(mAssistants).onUserStopped(eq(20)); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_invalidPackage() throws Exception { final String notReal = "NOT REAL"; final var checker = mService.permissionChecker; @@ -16303,6 +16355,25 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_invalidPackage_concurrent_multiUser() + throws Exception { + final String notReal = "NOT REAL"; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(notReal), anyInt())).thenThrow( + PackageManager.NameNotFoundException.class); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(notReal)).isFalse(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(notReal), anyInt()); + verify(checker, never()).check(any(), anyInt(), anyInt(), anyBoolean()); + verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(notReal), anyInt()); + verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt()); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_hasPermission() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16321,6 +16392,27 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_hasPermission_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(checker.check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true)) + .thenReturn(PackageManager.PERMISSION_GRANTED); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt()); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_isPackageAllowed() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16339,6 +16431,27 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_isPackageAllowed_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mConditionProviders.isPackageOrComponentAllowed(eq(packageName), anyInt())) + .thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt()); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_isComponentEnabled() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16356,6 +16469,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_isComponentEnabled_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mListeners.isComponentEnabledForPackage(packageName, mUserId)).thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName, mUserId); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_isDeviceOwner() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16372,10 +16505,30 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mDevicePolicyManager).isActiveDeviceOwner(uid); } + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_isDeviceOwner_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mDevicePolicyManager.isActiveDeviceOwner(uid)).thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName, mUserId); + verify(mDevicePolicyManager).isActiveDeviceOwner(uid); + } + /** * b/292163859 */ @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_callerIsDeviceOwner() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16394,7 +16547,32 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid); } + /** + * b/292163859 + */ @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_callerIsDeviceOwner_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final int callingUid = Binder.getCallingUid(); + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mDevicePolicyManager.isActiveDeviceOwner(callingUid)).thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName, mUserId); + verify(mDevicePolicyManager).isActiveDeviceOwner(uid); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_notGranted() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16411,6 +16589,24 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_notGranted_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName, mUserId); + verify(mDevicePolicyManager).isActiveDeviceOwner(uid); + } + + @Test public void testResetDefaultDnd() { TestableNotificationManagerService service = spy(mService); UserInfo user = new UserInfo(0, "owner", 0); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 3f26cd9258af..640de174ba20 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -3096,6 +3096,67 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void getPackagesWithAnyChannels_noChannels() { + assertThat(mHelper.getPackagesWithAnyChannels(UserHandle.getUserId(UID_O))).isEmpty(); + } + + @Test + public void getPackagesWithAnyChannels_someChannels() { + // 2 channels under PKG_N_MR1, 1 under PKG_O + NotificationChannel channel1 = new NotificationChannel("1", "something", + IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, UID_N_MR1, + false); + NotificationChannel channel2 = new NotificationChannel("2", "another", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false, UID_N_MR1, + false); + + NotificationChannel other = new NotificationChannel("3", "still another", + IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_O, UID_O, other, true, false, UID_O, false); + + assertThat(mHelper.getPackagesWithAnyChannels(USER.getIdentifier())).containsExactly( + PKG_N_MR1, PKG_O); + } + + @Test + public void getPackagesWithAnyChannels_onlyDeleted() { + NotificationChannel channel1 = new NotificationChannel("1", "something", + IMPORTANCE_DEFAULT); + channel1.setDeleted(true); + mHelper.createNotificationChannel(PKG_O, UID_O, channel1, true, false, UID_O, + false); + NotificationChannel channel2 = new NotificationChannel("2", "another", IMPORTANCE_DEFAULT); + channel2.setDeleted(true); + mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false, UID_O, + false); + + assertThat(mHelper.getPackagesWithAnyChannels(UserHandle.getUserId(UID_O))).isEmpty(); + } + + @Test + public void getPackagesWithAnyChannels_distinguishesUsers() throws Exception { + // Set a package up for both users 0 and 10 + String pkgName = "test.package"; + int uid0 = UserHandle.getUid(0, 1234); + int uid10 = UserHandle.getUid(10, 1234); + setUpPackageWithUid(pkgName, uid0); + setUpPackageWithUid(pkgName, uid10); + + // but only user 10 has channels + NotificationChannel channel1 = new NotificationChannel("1", "something", + IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(pkgName, uid10, channel1, true, false, uid10, + false); + NotificationChannel channel2 = new NotificationChannel("2", "another", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(pkgName, uid10, channel2, true, false, uid10, + false); + + assertThat(mHelper.getPackagesWithAnyChannels(0)).isEmpty(); + assertThat(mHelper.getPackagesWithAnyChannels(10)).containsExactly(pkgName); + } + + @Test public void testOnlyHasDefaultChannel() throws Exception { assertTrue(mHelper.onlyHasDefaultChannel(PKG_N_MR1, UID_N_MR1)); assertFalse(mHelper.onlyHasDefaultChannel(PKG_O, UID_O)); diff --git a/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt index 993c3fed9d59..2eb8ba1be811 100644 --- a/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt +++ b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt @@ -16,6 +16,7 @@ package android.animation +import android.content.pm.PackageManager import android.graphics.Color import android.platform.test.annotations.MotionTest import android.view.ViewGroup @@ -23,11 +24,14 @@ import android.widget.FrameLayout import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.internal.dynamicanimation.animation.DynamicAnimation import com.android.internal.dynamicanimation.animation.SpringAnimation import com.android.internal.dynamicanimation.animation.SpringForce import kotlinx.coroutines.test.TestScope +import org.junit.Assume.assumeFalse +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -65,6 +69,16 @@ class AnimatorTestRuleToolkitTest { bitmapDiffer = screenshotRule, ) + @Before + fun setUp() { + // Do not run on Automotive. + assumeFalse( + InstrumentationRegistry.getInstrumentation().context.packageManager.hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE + ) + ) + } + @Test fun recordFilmstrip_withAnimator() { val animatedBox = createScene() @@ -188,12 +202,7 @@ class AnimatorTestRuleToolkitTest { null } return motionRule.recordMotion( - AnimatorRuleRecordingSpec( - container, - motionControl, - sampleIntervalMs, - visualCapture, - ) { + AnimatorRuleRecordingSpec(container, motionControl, sampleIntervalMs, visualCapture) { feature(ViewFeatureCaptures.alpha, "alpha") } ) |