diff options
95 files changed, 1960 insertions, 826 deletions
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 1ddec17e49bb..0f54cb7bc35e 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -10476,10 +10476,6 @@ public class DevicePolicyManager { @WorkerThread public void setApplicationRestrictions(@Nullable ComponentName admin, String packageName, Bundle settings) { - if (!Flags.dmrhSetAppRestrictions()) { - throwIfParentInstance("setApplicationRestrictions"); - } - if (mService != null) { try { mService.setApplicationRestrictions(admin, mContext.getPackageName(), packageName, @@ -11884,9 +11880,6 @@ public class DevicePolicyManager { @WorkerThread public @NonNull Bundle getApplicationRestrictions( @Nullable ComponentName admin, String packageName) { - if (!Flags.dmrhSetAppRestrictions()) { - throwIfParentInstance("getApplicationRestrictions"); - } if (mService != null) { try { @@ -14231,21 +14224,11 @@ public class DevicePolicyManager { */ public @NonNull DevicePolicyManager getParentProfileInstance(@NonNull ComponentName admin) { throwIfParentInstance("getParentProfileInstance"); - try { - if (Flags.dmrhSetAppRestrictions()) { - UserManager um = mContext.getSystemService(UserManager.class); - if (!um.isManagedProfile()) { - throw new SecurityException("The current user does not have a parent profile."); - } - } else { - if (!mService.isManagedProfile(admin)) { - throw new SecurityException("The current user does not have a parent profile."); - } - } - return new DevicePolicyManager(mContext, mService, true); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + UserManager um = mContext.getSystemService(UserManager.class); + if (!um.isManagedProfile()) { + throw new SecurityException("The current user does not have a parent profile."); } + return new DevicePolicyManager(mContext, mService, true); } /** diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index d9bd77fb3d54..540592ff0b90 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -210,16 +210,6 @@ flag { } flag { - name: "dmrh_set_app_restrictions" - namespace: "enterprise" - description: "Allow DMRH to set application restrictions (both on the profile and the parent)" - bug: "328758346" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "always_persist_do" namespace: "enterprise" description: "Always write device_owners2.xml so that migration flags aren't lost" diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 22a9ccf425c2..297fe8a9e691 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -111,3 +111,10 @@ flag { description: "Device awareness in power and display APIs" bug: "285020111" } + +flag { + name: "status_bar_and_insets" + namespace: "virtual_devices" + description: "Allow for status bar and insets on virtual devices" + bug: "350007866" +} diff --git a/core/java/android/os/ArtModuleServiceManager.java b/core/java/android/os/ArtModuleServiceManager.java index e0b631d69ca8..995094bb1318 100644 --- a/core/java/android/os/ArtModuleServiceManager.java +++ b/core/java/android/os/ArtModuleServiceManager.java @@ -37,10 +37,12 @@ public class ArtModuleServiceManager { /** A class that exposes the method to obtain each system service. */ public static final class ServiceRegisterer { @NonNull private final String mServiceName; + private final boolean mRetry; /** @hide */ - public ServiceRegisterer(@NonNull String serviceName) { + public ServiceRegisterer(@NonNull String serviceName, boolean retry) { mServiceName = serviceName; + mRetry = retry; } /** @@ -53,27 +55,47 @@ public class ArtModuleServiceManager { */ @Nullable public IBinder waitForService() { - return ServiceManager.waitForService(mServiceName); + if (mRetry) { + return ServiceManager.waitForService(mServiceName); + } + IBinder binder = ServiceManager.getService(mServiceName); + for (int remainingTimeMs = 5000; binder == null && remainingTimeMs > 0; + remainingTimeMs -= 100) { + // There can be a race: + // 1. Client A invokes "ctl.start", which starts the service. + // 2. Client A gets a service handle from `ServiceManager.getService`. + // 3. Client B invokes "ctl.start", which does nothing because the service is + // already running. + // 4. Client A drops the service handle. The service is notified that there is no + // more client at that point, so it shuts down itself. + // 5. Client B cannot get a service handle from `ServiceManager.getService` because + // the service is shut down. + // To address this problem, we invoke "ctl.start" repeatedly. + SystemProperties.set("ctl.start", mServiceName); + SystemClock.sleep(100); + binder = ServiceManager.getService(mServiceName); + } + return binder; } } /** Returns {@link ServiceRegisterer} for the "artd" service. */ @NonNull public ServiceRegisterer getArtdServiceRegisterer() { - return new ServiceRegisterer("artd"); + return new ServiceRegisterer("artd", true /* retry */); } /** Returns {@link ServiceRegisterer} for the "artd_pre_reboot" service. */ @NonNull @FlaggedApi(Flags.FLAG_USE_ART_SERVICE_V2) public ServiceRegisterer getArtdPreRebootServiceRegisterer() { - return new ServiceRegisterer("artd_pre_reboot"); + return new ServiceRegisterer("artd_pre_reboot", false /* retry */); } /** Returns {@link ServiceRegisterer} for the "dexopt_chroot_setup" service. */ @NonNull @FlaggedApi(Flags.FLAG_USE_ART_SERVICE_V2) public ServiceRegisterer getDexoptChrootSetupServiceRegisterer() { - return new ServiceRegisterer("dexopt_chroot_setup"); + return new ServiceRegisterer("dexopt_chroot_setup", true /* retry */); } } diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index ebf87f1d1dba..cab6d8e9774a 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -245,3 +245,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_desktop_windowing_persistence" + namespace: "lse_desktop_experience" + description: "Persists the desktop windowing session across reboots." + bug: "350456942" +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 972b78f6ca9a..6146ecd9ade6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -831,7 +831,6 @@ public class CompatUIController implements OnDisplaysChangedListener, */ static class CompatUIHintsState { boolean mHasShownSizeCompatHint; - boolean mHasShownCameraCompatHint; boolean mHasShownUserAspectRatioSettingsButtonHint; } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 1930c3d8b9b7..b84990b54bd5 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -288,8 +288,7 @@ public final class MediaRouter2 { /** * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app - * specified by {@code clientPackageName}. Returns {@code null} if the specified package name - * does not exist. + * specified by {@code clientPackageName}. * * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances: * diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index 2b3862f88c07..34b597ba4486 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -129,3 +129,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "member_device_lea_active_state_sync_fix" + namespace: "cross_device_experiences" + description: "Gates whether to enable fix for member device active state sync on lea profile" + bug: "364201289" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SettingsLib/res/values/config.xml b/packages/SettingsLib/res/values/config.xml index 68b81db1d9c0..3c3de044cc4e 100644 --- a/packages/SettingsLib/res/values/config.xml +++ b/packages/SettingsLib/res/values/config.xml @@ -31,4 +31,14 @@ <!-- Control whether status bar should distinguish HSPA data icon form UMTS data icon on devices --> <bool name="config_hspa_data_distinguishable">false</bool> + + <!-- Edit User avatar explicit package name --> + <string name="config_avatar_picker_package" translatable="false"> + com.android.avatarpicker + </string> + + <!-- Edit User avatar explicit activity class --> + <string name="config_avatar_picker_class" translatable="false"> + com.android.avatarpicker.ui.AvatarPickerActivity + </string> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java index cdc3f123eff7..f38e91ac0d8a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java @@ -32,6 +32,7 @@ import androidx.annotation.Nullable; import com.android.internal.util.UserIcons; import com.android.settingslib.drawable.CircleFramedDrawable; +import com.android.settingslib.R; import com.android.settingslib.utils.ThreadUtils; import com.google.common.util.concurrent.FutureCallback; @@ -132,6 +133,13 @@ public class EditUserPhotoController { intent.addCategory(Intent.CATEGORY_DEFAULT); if (Flags.avatarSync()) { intent.putExtra(EXTRA_IS_USER_NEW, isUserNew); + // Fix vulnerability b/341688848 by explicitly set the class name of avatar picker. + if (Flags.fixAvatarCrossUserLeak()) { + final String packageName = + mActivity.getString(R.string.config_avatar_picker_package); + final String className = mActivity.getString(R.string.config_avatar_picker_class); + intent.setClassName(packageName, className); + } } else { // SettingsLib is used by multiple apps therefore we need to know out of all apps // using settingsLib which one is the one we return value to. diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp index c60eb61c57eb..fb1f715bc68f 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp +++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp @@ -41,6 +41,7 @@ android_app { "SettingsLibDisplayUtils", "SettingsLibSettingsTheme", "com_android_a11y_menu_flags_lib", + "//frameworks/libs/systemui:view_capture", ], optimize: { diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java index 448472d1b6e4..3db61a58c7a3 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java @@ -22,6 +22,8 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; +import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance; + import static java.lang.Math.max; import android.animation.Animator; @@ -53,6 +55,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.UiContext; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; import com.android.systemui.accessibility.accessibilitymenu.Flags; import com.android.systemui.accessibility.accessibilitymenu.R; @@ -143,7 +146,9 @@ public class A11yMenuOverlayLayout { final Display display = mDisplayManager.getDisplay(DEFAULT_DISPLAY); final Context uiContext = mService.createWindowContext( display, TYPE_ACCESSIBILITY_OVERLAY, /* options= */null); - final WindowManager windowManager = uiContext.getSystemService(WindowManager.class); + final ViewCaptureAwareWindowManager windowManager = + getViewCaptureAwareWindowManagerInstance(uiContext, + com.android.systemui.Flags.enableViewCaptureTracing()); mLayout = new A11yMenuFrameLayout(uiContext); updateLayoutPosition(uiContext); inflateLayoutAndSetOnTouchListener(mLayout, uiContext); @@ -158,8 +163,8 @@ public class A11yMenuOverlayLayout { public void clearLayout() { if (mLayout != null) { - WindowManager windowManager = - mLayout.getContext().getSystemService(WindowManager.class); + ViewCaptureAwareWindowManager windowManager = getViewCaptureAwareWindowManagerInstance( + mLayout.getContext(), com.android.systemui.Flags.enableViewCaptureTracing()); if (windowManager != null) { windowManager.removeView(mLayout); } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index c5bb33c414b1..7fb88e8d1fcc 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -27,8 +27,8 @@ import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.bouncer.ui.BouncerDialogFactory -import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneActionsViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.ExclusiveActivatable @@ -54,18 +54,17 @@ object Bouncer { class BouncerScene @Inject constructor( - private val actionsViewModelFactory: BouncerSceneActionsViewModel.Factory, + private val actionsViewModelFactory: BouncerUserActionsViewModel.Factory, private val contentViewModelFactory: BouncerSceneContentViewModel.Factory, private val dialogFactory: BouncerDialogFactory, ) : ExclusiveActivatable(), Scene { override val key = Scenes.Bouncer - private val actionsViewModel: BouncerSceneActionsViewModel by lazy { + private val actionsViewModel: BouncerUserActionsViewModel by lazy { actionsViewModelFactory.create() } - override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - actionsViewModel.actions + override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions override suspend fun onActivated(): Nothing { actionsViewModel.activate() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt index f658169a24ff..8b6de6ab22f3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt @@ -49,10 +49,10 @@ constructor( ) : ExclusiveActivatable(), Scene { override val key = Scenes.Communal - override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - MutableStateFlow<Map<UserAction, UserActionResult>>( + override val userActions: Flow<Map<UserAction, UserActionResult>> = + MutableStateFlow( mapOf( - Swipe(SwipeDirection.End) to UserActionResult(Scenes.Lockscreen), + Swipe(SwipeDirection.End) to Scenes.Lockscreen, ) ) .asStateFlow() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index 5f600d3002cc..c7c29f9fdb7c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -24,7 +24,7 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateContentFloatAsState import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneActionsViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenUserActionsViewModel import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.scene.shared.model.Scenes @@ -38,17 +38,16 @@ import kotlinx.coroutines.flow.Flow class LockscreenScene @Inject constructor( - actionsViewModelFactory: LockscreenSceneActionsViewModel.Factory, + actionsViewModelFactory: LockscreenUserActionsViewModel.Factory, private val lockscreenContent: Lazy<LockscreenContent>, ) : ExclusiveActivatable(), Scene { override val key = Scenes.Lockscreen - private val actionsViewModel: LockscreenSceneActionsViewModel by lazy { + private val actionsViewModel: LockscreenUserActionsViewModel by lazy { actionsViewModelFactory.create() } - override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - actionsViewModel.actions + override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions override suspend fun onActivated(): Nothing { actionsViewModel.activate() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt index e4c611ee0eb2..a22beccf3448 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -23,6 +23,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.rememberViewModel @@ -39,6 +41,7 @@ import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.flow.Flow @SysUISingleton class NotificationsShadeOverlay @@ -59,6 +62,8 @@ constructor( actionsViewModelFactory.create() } + override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions + override suspend fun activate(): Nothing { actionsViewModel.activate() } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt index ea3f066960c1..1f4cd0473086 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt @@ -29,7 +29,7 @@ import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel -import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneActionsViewModel +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeUserActionsViewModel import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Scene @@ -49,7 +49,7 @@ import kotlinx.coroutines.flow.Flow class NotificationsShadeScene @Inject constructor( - private val actionsViewModelFactory: NotificationsShadeSceneActionsViewModel.Factory, + private val actionsViewModelFactory: NotificationsShadeUserActionsViewModel.Factory, private val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, private val tintedIconManagerFactory: TintedIconManager.Factory, @@ -61,12 +61,11 @@ constructor( override val key = Scenes.NotificationsShade - private val actionsViewModel: NotificationsShadeSceneActionsViewModel by lazy { + private val actionsViewModel: NotificationsShadeUserActionsViewModel by lazy { actionsViewModelFactory.create() } - override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - actionsViewModel.actions + override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions override suspend fun onActivated(): Nothing { actionsViewModel.activate() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 373383f0a2fe..d34295ea1d22 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -93,8 +93,8 @@ import com.android.systemui.notifications.ui.composable.NotificationStackCutoffG import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.InQS -import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneActionsViewModel import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneContentViewModel +import com.android.systemui.qs.ui.viewmodel.QuickSettingsUserActionsViewModel import com.android.systemui.res.R import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes @@ -125,7 +125,7 @@ constructor( private val shadeSession: SaveableSession, private val notificationStackScrollView: Lazy<NotificationScrollView>, private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, - private val actionsViewModelFactory: QuickSettingsSceneActionsViewModel.Factory, + private val actionsViewModelFactory: QuickSettingsUserActionsViewModel.Factory, private val contentViewModelFactory: QuickSettingsSceneContentViewModel.Factory, private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, @@ -135,12 +135,11 @@ constructor( ) : ExclusiveActivatable(), Scene { override val key = Scenes.QuickSettings - private val actionsViewModel: QuickSettingsSceneActionsViewModel by lazy { + private val actionsViewModel: QuickSettingsUserActionsViewModel by lazy { actionsViewModelFactory.create() } - override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - actionsViewModel.actions + override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions override suspend fun onActivated(): Nothing { actionsViewModel.activate() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index 988c712b7980..f8d0588c9ae6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -34,6 +34,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.compose.modifiers.sysuiResTag @@ -51,6 +53,7 @@ import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager import javax.inject.Inject +import kotlinx.coroutines.flow.Flow @SysUISingleton class QuickSettingsShadeOverlay @@ -69,6 +72,8 @@ constructor( actionsViewModelFactory.create() } + override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions + override suspend fun activate(): Nothing { actionsViewModel.activate() } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt index 9316eb90a7a2..e27c7e29ba50 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt @@ -27,8 +27,8 @@ import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel -import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneActionsViewModel import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneContentViewModel +import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeUserActionsViewModel import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.shade.ui.composable.ExpandedShadeHeader @@ -43,7 +43,7 @@ import kotlinx.coroutines.flow.Flow class QuickSettingsShadeScene @Inject constructor( - private val actionsViewModelFactory: QuickSettingsShadeSceneActionsViewModel.Factory, + private val actionsViewModelFactory: QuickSettingsShadeUserActionsViewModel.Factory, private val contentViewModelFactory: QuickSettingsShadeSceneContentViewModel.Factory, private val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, private val tintedIconManagerFactory: TintedIconManager.Factory, @@ -53,12 +53,11 @@ constructor( override val key = Scenes.QuickSettingsShade - private val actionsViewModel: QuickSettingsShadeSceneActionsViewModel by lazy { + private val actionsViewModel: QuickSettingsShadeUserActionsViewModel by lazy { actionsViewModelFactory.create() } - override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - actionsViewModel.actions + override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions override suspend fun onActivated(): Nothing { actionsViewModel.activate() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ActionableContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ActionableContent.kt new file mode 100644 index 000000000000..8fe6893cb352 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ActionableContent.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.ui.composable + +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import kotlinx.coroutines.flow.Flow + +/** Defines interface for content that can respond to user-actions. */ +interface ActionableContent { + /** + * The mapping between [UserAction] and destination [UserActionResult]s. + * + * When the scene framework detects a user action, if the current scene has a map entry for that + * user action, the framework starts a transition to the content specified in the map. + * + * Once the content is shown, the scene framework will read this property and set up a collector + * to watch for new mapping values. For each map entry, the scene framework will set up user + * input handling for its [UserAction] and, if such a user action is detected, initiate a + * transition to the specified [UserActionResult]. + * + * Note that reading from this method does _not_ mean that any user action has occurred. + * Instead, the property is read before any user action/gesture is detected so that the + * framework can decide whether to set up gesture/input detectors/listeners in case user actions + * of the given types ever occur. + * + * A missing value for a specific [UserAction] means that the user action of the given type is + * not currently active in the top-most content (in z-index order) and should be ignored by the + * framework until the top-most content changes. + */ + val userActions: Flow<Map<UserAction, UserActionResult>> +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index 6fb4724426fc..ae5dd8abb82e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -33,7 +33,7 @@ import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.Default import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.ui.viewmodel.GoneSceneActionsViewModel +import com.android.systemui.scene.ui.viewmodel.GoneUserActionsViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import dagger.Lazy @@ -50,14 +50,13 @@ class GoneScene constructor( private val notificationStackScrolLView: Lazy<NotificationScrollView>, private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, - private val viewModelFactory: GoneSceneActionsViewModel.Factory, + private val viewModelFactory: GoneUserActionsViewModel.Factory, ) : ExclusiveActivatable(), Scene { override val key = Scenes.Gone - private val actionsViewModel: GoneSceneActionsViewModel by lazy { viewModelFactory.create() } + private val actionsViewModel: GoneUserActionsViewModel by lazy { viewModelFactory.create() } - override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - actionsViewModel.actions + override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions override suspend fun onActivated(): Nothing { actionsViewModel.activate() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Overlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Overlay.kt index d62befd10745..609ce90fd684 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Overlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Overlay.kt @@ -18,9 +18,14 @@ package com.android.systemui.scene.ui.composable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.OverlayKey +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult import com.android.systemui.lifecycle.Activatable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf /** * Defines interface for classes that can describe an "overlay". @@ -29,9 +34,17 @@ import com.android.systemui.lifecycle.Activatable * container takes care of rendering any current overlays and allowing overlays to be shown, hidden, * or replaced based on a user action. */ -interface Overlay : Activatable { +interface Overlay : Activatable, ActionableContent { /** Uniquely-identifying key for this overlay. The key must be unique within its container. */ val key: OverlayKey + /** + * The user actions supported by this overlay. + * + * @see [ActionableContent.userActions] + */ + override val userActions: Flow<Map<UserAction, UserActionResult>> + get() = flowOf(mapOf(Back to UserActionResult.HideOverlay(key))) + @Composable fun ContentScope.Content(modifier: Modifier) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Scene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Scene.kt index 5319ec345d00..8d8ab8ee7949 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Scene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Scene.kt @@ -20,10 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneScope -import com.android.compose.animation.scene.UserAction -import com.android.compose.animation.scene.UserActionResult import com.android.systemui.lifecycle.Activatable -import kotlinx.coroutines.flow.Flow /** * Defines interface for classes that can describe a "scene". @@ -33,32 +30,10 @@ import kotlinx.coroutines.flow.Flow * based on either user action (for example, swiping down while on the lock screen scene may switch * to the shade scene). */ -interface Scene : Activatable { +interface Scene : Activatable, ActionableContent { /** Uniquely-identifying key for this scene. The key must be unique within its container. */ val key: SceneKey - /** - * The mapping between [UserAction] and destination [UserActionResult]s. - * - * When the scene framework detects a user action, if the current scene has a map entry for that - * user action, the framework starts a transition to the scene in the map. - * - * Once the [Scene] becomes the current one, the scene framework will read this property and set - * up a collector to watch for new mapping values. If every map entry provided by the scene, the - * framework will set up user input handling for its [UserAction] and, if such a user action is - * detected, initiate a transition to the specified [UserActionResult]. - * - * Note that reading from this method does _not_ mean that any user action has occurred. - * Instead, the property is read before any user action/gesture is detected so that the - * framework can decide whether to set up gesture/input detectors/listeners in case user actions - * of the given types ever occur. - * - * Note that a missing value for a specific [UserAction] means that the user action of the given - * type is not currently active in the scene and should be ignored by the framework, while the - * current scene is this one. - */ - val destinationScenes: Flow<Map<UserAction, UserActionResult>> - @Composable fun SceneScope.Content(modifier: Modifier) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index 851fa3ff005e..a7e41ce05f58 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -104,7 +104,7 @@ fun SceneContainer( // TODO(b/359173565): Add overlay user actions when the API is final. LaunchedEffect(currentSceneKey) { try { - sceneByKey[currentSceneKey]?.destinationScenes?.collectLatest { userActions -> + sceneByKey[currentSceneKey]?.userActions?.collectLatest { userActions -> userActionsByContentKey[currentSceneKey] = viewModel.resolveSceneFamilies(userActions) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 5fcf5226a585..a03bf43c4c8f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -108,8 +108,8 @@ import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.shade.shared.model.ShadeMode -import com.android.systemui.shade.ui.viewmodel.ShadeSceneActionsViewModel import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeUserActionsViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import com.android.systemui.statusbar.phone.StatusBarLocation @@ -154,7 +154,7 @@ class ShadeScene constructor( private val shadeSession: SaveableSession, private val notificationStackScrollView: Lazy<NotificationScrollView>, - private val actionsViewModelFactory: ShadeSceneActionsViewModel.Factory, + private val actionsViewModelFactory: ShadeUserActionsViewModel.Factory, private val contentViewModelFactory: ShadeSceneContentViewModel.Factory, private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, private val tintedIconManagerFactory: TintedIconManager.Factory, @@ -167,7 +167,7 @@ constructor( override val key = Scenes.Shade - private val actionsViewModel: ShadeSceneActionsViewModel by lazy { + private val actionsViewModel: ShadeUserActionsViewModel by lazy { actionsViewModelFactory.create() } @@ -175,8 +175,7 @@ constructor( actionsViewModel.activate() } - override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - actionsViewModel.actions + override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions @Composable override fun SceneScope.Content( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt index 4aa50b586c1b..b30f2b7002ce 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt @@ -399,26 +399,53 @@ private class AnimatedStateImpl<T, Delta>( val fromValue = sharedValue[transition.fromContent] val toValue = sharedValue[transition.toContent] - return if (fromValue != null && toValue != null) { - if (fromValue == toValue) { - // Optimization: avoid reading progress if the values are the same, so we don't - // relayout/redraw for nothing. - fromValue - } else { - val overscrollSpec = transition.currentOverscrollSpec - val progress = - when { - overscrollSpec == null -> { - if (canOverflow) transition.progress - else transition.progress.fastCoerceIn(0f, 1f) - } - overscrollSpec.content == transition.toContent -> 1f - else -> 0f - } - - sharedValue.type.lerp(fromValue, toValue, progress) + if (fromValue == null && toValue == null) { + return null + } + + if (fromValue != null && toValue != null) { + return interpolateSharedValue(fromValue, toValue, transition, sharedValue) + } + + if (transition is TransitionState.Transition.ReplaceOverlay) { + val currentSceneValue = sharedValue[transition.currentScene] + if (currentSceneValue != null) { + return interpolateSharedValue( + fromValue = fromValue ?: currentSceneValue, + toValue = toValue ?: currentSceneValue, + transition, + sharedValue, + ) + } + } + + return fromValue ?: toValue + } + + private fun interpolateSharedValue( + fromValue: T, + toValue: T, + transition: TransitionState.Transition, + sharedValue: SharedValue<T, *>, + ): T? { + if (fromValue == toValue) { + // Optimization: avoid reading progress if the values are the same, so we don't + // relayout/redraw for nothing. + return fromValue + } + + val overscrollSpec = transition.currentOverscrollSpec + val progress = + when { + overscrollSpec == null -> { + if (canOverflow) transition.progress + else transition.progress.fastCoerceIn(0f, 1f) + } + overscrollSpec.content == transition.toContent -> 1f + else -> 0f } - } else fromValue ?: toValue + + return sharedValue.type.lerp(fromValue, toValue, progress) } private fun transition(sharedValue: SharedValue<T, Delta>): TransitionState.Transition? { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index 9b1740dc700a..4c0feb883c84 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -313,10 +313,27 @@ internal class ElementNode( // If this element is not supposed to be laid out now, either because it is not part of any // ongoing transition or the other content of its transition is overscrolling, then lay out // the element normally and don't place it. - val overscrollScene = transition?.currentOverscrollSpec?.content - val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != content.key - if (isOtherSceneOverscrolling) { - return doNotPlace(measurable, constraints) + val overscrollContent = transition?.currentOverscrollSpec?.content + if (overscrollContent != null && overscrollContent != content.key) { + when (transition) { + is TransitionState.Transition.ChangeScene -> + return doNotPlace(measurable, constraints) + + // If we are overscrolling an overlay that does not contain an element that is in + // the current scene, place it in that scene otherwise the element won't be placed + // at all. + is TransitionState.Transition.ShowOrHideOverlay, + is TransitionState.Transition.ReplaceOverlay -> { + if ( + content.key == transition.currentScene && + overscrollContent !in element.stateByContent + ) { + return placeNormally(measurable, constraints) + } else { + return doNotPlace(measurable, constraints) + } + } + } } val placeable = @@ -1230,17 +1247,30 @@ private inline fun <T> computeValue( // elements follow the finger direction. val isSharedElement = fromState != null && toState != null if (isSharedElement && isSharedElementEnabled(element.key, transition)) { - val start = contentValue(fromState!!) - val end = contentValue(toState!!) - - // TODO(b/316901148): Remove checks to isSpecified() once the lookahead pass runs for all - // nodes before the intermediate layout pass. - if (!isSpecified(start)) return end - if (!isSpecified(end)) return start + return interpolateSharedElement( + transition = transition, + contentValue = contentValue, + fromState = fromState!!, + toState = toState!!, + isSpecified = isSpecified, + lerp = lerp, + ) + } - // Make sure we don't read progress if values are the same and we don't need to interpolate, - // so we don't invalidate the phase where this is read. - return if (start == end) start else lerp(start, end, transition.progress) + // If we are replacing an overlay and the element is both in a single overlay and in the current + // scene, interpolate the state of the element using the current scene as the other scene. + if (!isSharedElement && transition is TransitionState.Transition.ReplaceOverlay) { + val currentSceneState = element.stateByContent[transition.currentScene] + if (currentSceneState != null) { + return interpolateSharedElement( + transition = transition, + contentValue = contentValue, + fromState = fromState ?: currentSceneState, + toState = toState ?: currentSceneState, + isSpecified = isSpecified, + lerp = lerp, + ) + } } // Get the transformed value, i.e. the target value at the beginning (for entering elements) or @@ -1383,3 +1413,24 @@ private inline fun <T> computeValue( lerp(idleValue, targetValue, rangeProgress) } } + +private inline fun <T> interpolateSharedElement( + transition: TransitionState.Transition, + contentValue: (Element.State) -> T, + fromState: Element.State, + toState: Element.State, + isSpecified: (T) -> Boolean, + lerp: (T, T, Float) -> T +): T { + val start = contentValue(fromState) + val end = contentValue(toState) + + // TODO(b/316901148): Remove checks to isSpecified() once the lookahead pass runs for all + // nodes before the intermediate layout pass. + if (!isSpecified(start)) return end + if (!isSpecified(end)) return start + + // Make sure we don't read progress if values are the same and we don't need to interpolate, + // so we don't invalidate the phase where this is read. + return if (start == end) start else lerp(start, end, transition.progress) +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt index bec2bb2baa3c..c25478b35790 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt @@ -22,10 +22,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -42,6 +44,8 @@ import com.android.compose.animation.scene.TestOverlays.OverlayA import com.android.compose.animation.scene.TestOverlays.OverlayB import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.test.assertSizeIsEqualTo +import com.android.compose.test.subjects.assertThat +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import org.junit.Rule import org.junit.Test @@ -524,4 +528,124 @@ class OverlayTest { } } } + + @Test + fun replaceAnimation_elementInCurrentSceneAndOneOverlay() { + val sharedIntKey = ValueKey("sharedInt") + val sharedIntValueByContent = mutableMapOf<ContentKey, Int>() + + @Composable + fun SceneScope.animateContentInt(targetValue: Int) { + val animatedValue = animateContentIntAsState(targetValue, sharedIntKey) + LaunchedEffect(animatedValue) { + try { + snapshotFlow { animatedValue.value } + .collect { sharedIntValueByContent[contentKey] = it } + } finally { + sharedIntValueByContent.remove(contentKey) + } + } + } + + rule.testReplaceOverlayTransition( + currentSceneContent = { + Box(Modifier.size(width = 180.dp, height = 120.dp)) { + animateContentInt(targetValue = 1_000) + Foo(width = 60.dp, height = 40.dp) + } + }, + fromContent = {}, + fromAlignment = Alignment.TopStart, + toContent = { + animateContentInt(targetValue = 2_000) + Foo(width = 100.dp, height = 80.dp) + }, + transition = { + // 4 frames of animation + spec = tween(4 * 16, easing = LinearEasing) + }, + ) { + // Foo moves from (0,0) with a size of 60x40dp to centered (in a 180x120dp Box) with a + // size of 100x80dp, so at (40,20). + // + // The animated Int goes from 1_000 to 2_000. + before { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertSizeIsEqualTo(60.dp, 40.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist() + + assertThat(sharedIntValueByContent).containsEntry(SceneA, 1_000) + assertThat(sharedIntValueByContent).doesNotContainKey(OverlayA) + assertThat(sharedIntValueByContent).doesNotContainKey(OverlayB) + } + + at(16) { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + rule + .onNode(isElement(TestElements.Foo, content = OverlayB)) + .assertSizeIsEqualTo(70.dp, 50.dp) + .assertPositionInRootIsEqualTo(10.dp, 5.dp) + + assertThat(sharedIntValueByContent).containsEntry(SceneA, 1_250) + assertThat(sharedIntValueByContent).doesNotContainKey(OverlayA) + assertThat(sharedIntValueByContent).containsEntry(OverlayB, 1_250) + } + + at(32) { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + rule + .onNode(isElement(TestElements.Foo, content = OverlayB)) + .assertSizeIsEqualTo(80.dp, 60.dp) + .assertPositionInRootIsEqualTo(20.dp, 10.dp) + + assertThat(sharedIntValueByContent).containsEntry(SceneA, 1_500) + assertThat(sharedIntValueByContent).doesNotContainKey(OverlayA) + assertThat(sharedIntValueByContent).containsEntry(OverlayB, 1_500) + } + + at(48) { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + rule + .onNode(isElement(TestElements.Foo, content = OverlayB)) + .assertSizeIsEqualTo(90.dp, 70.dp) + .assertPositionInRootIsEqualTo(30.dp, 15.dp) + + assertThat(sharedIntValueByContent).containsEntry(SceneA, 1_750) + assertThat(sharedIntValueByContent).doesNotContainKey(OverlayA) + assertThat(sharedIntValueByContent).containsEntry(OverlayB, 1_750) + } + + after { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + rule + .onNode(isElement(TestElements.Foo, content = OverlayB)) + .assertSizeIsEqualTo(100.dp, 80.dp) + .assertPositionInRootIsEqualTo(40.dp, 20.dp) + + // Outside of transitions, the value is equal to the target value in each content. + assertThat(sharedIntValueByContent).containsEntry(SceneA, 1_000) + assertThat(sharedIntValueByContent).doesNotContainKey(OverlayA) + assertThat(sharedIntValueByContent).containsEntry(OverlayB, 2_000) + } + } + } } diff --git a/packages/SystemUI/docs/scene.md b/packages/SystemUI/docs/scene.md index a7740c677d51..0ac15c583b29 100644 --- a/packages/SystemUI/docs/scene.md +++ b/packages/SystemUI/docs/scene.md @@ -124,8 +124,8 @@ Each scene is defined as an implementation of the [`Scene`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Scene.kt) interface, which has three parts: 1. The `key` property returns the [`SceneKey`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt) -that uniquely identifies that scene 2. The `destinationScenes` `Flow` returns -the (potentially ever-changing) set of navigation edges to other scenes, based +that uniquely identifies that scene 2. The `userActions` `Flow` returns +the (potentially ever-changing) set of navigation edges to other content, based on user-actions, which is how the navigation graph is defined (see [the Scene navigation](#Scene-navigation) section for more) 3. The `Content` function which uses @@ -141,7 +141,7 @@ For example: @SysUISingleton class YourScene @Inject constructor( /* your dependencies here */ ) : Scene { override val key = SceneKey.YourScene - override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = + override val userActions: StateFlow<Map<UserAction, SceneModel>> = MutableStateFlow<Map<UserAction, SceneModel>>( mapOf( // This is where scene navigation is defined, more on that below. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerUserActionsViewModelTest.kt index a86a0c022c21..f58bbc3cf0cf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerUserActionsViewModelTest.kt @@ -44,17 +44,17 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @EnableSceneContainer -class BouncerSceneActionsViewModelTest : SysuiTestCase() { +class BouncerUserActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private lateinit var underTest: BouncerSceneActionsViewModel + private lateinit var underTest: BouncerUserActionsViewModel @Before fun setUp() { kosmos.sceneContainerStartable.start() - underTest = kosmos.bouncerSceneActionsViewModel + underTest = kosmos.bouncerUserActionsViewModel underTest.activateIn(testScope) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index ca15eff4610b..34d926a23edb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt @@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.coroutines.collectLastValue import com.android.systemui.education.data.model.GestureEduModel @@ -220,17 +221,19 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { verify(kosmos.mockEduInputManager) .registerKeyGestureEventListener(any(), listenerCaptor.capture()) - val backGestureEvent = + val allAppsKeyGestureEvent = KeyGestureEvent( /* deviceId= */ 1, - intArrayOf(KeyEvent.KEYCODE_ESCAPE), + IntArray(0), KeyEvent.META_META_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_BACK + KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS ) - listenerCaptor.value.onKeyGestureEvent(backGestureEvent) + listenerCaptor.value.onKeyGestureEvent(allAppsKeyGestureEvent) val model by - collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK)) + collectLastValue( + kosmos.contextualEducationRepository.readGestureEduModelFlow(ALL_APPS) + ) assertThat(model?.lastShortcutTriggeredTime).isEqualTo(eduClock.instant()) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt index cd0c58feebed..8b5f59457e6e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt @@ -19,16 +19,23 @@ package com.android.systemui.education.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.contextualeducation.GestureType.BACK +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.education.data.repository.contextualEducationRepository import com.android.systemui.education.data.repository.fakeEduClock +import com.android.systemui.inputdevice.data.model.UserDeviceConnectionStatus +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -36,24 +43,190 @@ class KeyboardTouchpadStatsInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val underTest = kosmos.keyboardTouchpadEduStatsInteractor + private val repository = kosmos.contextualEducationRepository + private val fakeClock = kosmos.fakeEduClock + private val initialDelayElapsedDuration = + KeyboardTouchpadEduStatsInteractorImpl.initialDelayDuration + 1.seconds + + @Test + fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() = + testScope.runTest { + setUpForInitialDelayElapse() + whenever(mockUserInputDeviceRepository.isAnyTouchpadConnectedForUser) + .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = true, userId = 0))) + + val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(BACK) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountWhenTouchpadDisconnected() = + testScope.runTest { + setUpForInitialDelayElapse() + whenever(mockUserInputDeviceRepository.isAnyTouchpadConnectedForUser) + .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = false, userId = 0))) + + val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(BACK) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUpdatedOnIncrementSignalCountWhenKeyboardConnected() = + testScope.runTest { + setUpForInitialDelayElapse() + whenever(mockUserInputDeviceRepository.isAnyKeyboardConnectedForUser) + .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = true, userId = 0))) + + val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS)) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(ALL_APPS) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountWhenKeyboardDisconnected() = + testScope.runTest { + setUpForInitialDelayElapse() + whenever(mockUserInputDeviceRepository.isAnyKeyboardConnectedForUser) + .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = false, userId = 0))) + + val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS)) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(ALL_APPS) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUpdatedOnIncrementSignalCountAfterOobeLaunchInitialDelay() = + testScope.runTest { + setUpForDeviceConnection() + whenever(mockTutorialSchedulerRepository.launchTime(any<DeviceType>())) + .thenReturn(fakeClock.instant()) + fakeClock.offset(initialDelayElapsedDuration) + + val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(BACK) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } @Test - fun dataUpdatedOnIncrementSignalCount() = + fun dataUnchangedOnIncrementSignalCountBeforeOobeLaunchInitialDelay() = testScope.runTest { - val model by - collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK)) + setUpForDeviceConnection() + whenever(mockTutorialSchedulerRepository.launchTime(any<DeviceType>())) + .thenReturn(fakeClock.instant()) + + val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) val originalValue = model!!.signalCount underTest.incrementSignalCount(BACK) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUpdatedOnIncrementSignalCountAfterTouchpadConnectionInitialDelay() = + testScope.runTest { + setUpForDeviceConnection() + repository.updateEduDeviceConnectionTime { model -> + model.copy(touchpadFirstConnectionTime = fakeClock.instant()) + } + fakeClock.offset(initialDelayElapsedDuration) + + val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(BACK) + assertThat(model?.signalCount).isEqualTo(originalValue + 1) } @Test + fun dataUnchangedOnIncrementSignalCountBeforeTouchpadConnectionInitialDelay() = + testScope.runTest { + setUpForDeviceConnection() + repository.updateEduDeviceConnectionTime { model -> + model.copy(touchpadFirstConnectionTime = fakeClock.instant()) + } + + val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(BACK) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUpdatedOnIncrementSignalCountAfterKeyboardConnectionInitialDelay() = + testScope.runTest { + setUpForDeviceConnection() + repository.updateEduDeviceConnectionTime { model -> + model.copy(keyboardFirstConnectionTime = fakeClock.instant()) + } + fakeClock.offset(initialDelayElapsedDuration) + + val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS)) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(ALL_APPS) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountBeforeKeyboardConnectionInitialDelay() = + testScope.runTest { + setUpForDeviceConnection() + repository.updateEduDeviceConnectionTime { model -> + model.copy(keyboardFirstConnectionTime = fakeClock.instant()) + } + + val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS)) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(ALL_APPS) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUnchangedOnIncrementSignalCountWhenNoSetupTime() = + testScope.runTest { + whenever(mockUserInputDeviceRepository.isAnyTouchpadConnectedForUser) + .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = true, userId = 0))) + + val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(BACK) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test fun dataAddedOnUpdateShortcutTriggerTime() = testScope.runTest { - val model by - collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK)) + val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) assertThat(model?.lastShortcutTriggeredTime).isNull() underTest.updateShortcutTriggerTime(BACK) assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant()) } + + private suspend fun setUpForInitialDelayElapse() { + whenever(mockTutorialSchedulerRepository.launchTime(any<DeviceType>())) + .thenReturn(fakeClock.instant()) + fakeClock.offset(initialDelayElapsedDuration) + } + + private fun setUpForDeviceConnection() { + whenever(mockUserInputDeviceRepository.isAnyTouchpadConnectedForUser) + .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = true, userId = 0))) + whenever(mockUserInputDeviceRepository.isAnyKeyboardConnectedForUser) + .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = true, userId = 0))) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt index c66ebf3a31e0..4253c29d241c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt @@ -63,7 +63,7 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) @RunWithLooper @EnableSceneContainer -class LockscreenSceneActionsViewModelTest : SysuiTestCase() { +class LockscreenUserActionsViewModelTest : SysuiTestCase() { companion object { private const val parameterCount = 6 @@ -170,7 +170,7 @@ class LockscreenSceneActionsViewModelTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_COMMUNAL_HUB) - fun destinationScenes() = + fun userActions() = testScope.runTest { underTest.activateIn(this) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) @@ -193,9 +193,9 @@ class LockscreenSceneActionsViewModelTest : SysuiTestCase() { }, ) - val destinationScenes by collectLastValue(underTest.actions) + val userActions by collectLastValue(underTest.actions) val downDestination = - destinationScenes?.get( + userActions?.get( Swipe( SwipeDirection.Down, fromSource = Edge.Top.takeIf { downFromEdge }, @@ -227,11 +227,10 @@ class LockscreenSceneActionsViewModelTest : SysuiTestCase() { val upScene by collectLastValue( - (destinationScenes?.get(Swipe(SwipeDirection.Up)) - as? UserActionResult.ChangeScene) - ?.toScene - ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) } - ?: flowOf(null) + (userActions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene?.let { + scene -> + kosmos.sceneInteractor.resolveSceneFamily(scene) + } ?: flowOf(null) ) assertThat(upScene) @@ -244,11 +243,10 @@ class LockscreenSceneActionsViewModelTest : SysuiTestCase() { val leftScene by collectLastValue( - (destinationScenes?.get(Swipe(SwipeDirection.Left)) - as? UserActionResult.ChangeScene) - ?.toScene - ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) } - ?: flowOf(null) + (userActions?.get(Swipe.Left) as? UserActionResult.ChangeScene)?.toScene?.let { + scene -> + kosmos.sceneInteractor.resolveSceneFamily(scene) + } ?: flowOf(null) ) assertThat(leftScene) @@ -260,8 +258,8 @@ class LockscreenSceneActionsViewModelTest : SysuiTestCase() { ) } - private fun createLockscreenSceneViewModel(): LockscreenSceneActionsViewModel { - return LockscreenSceneActionsViewModel( + private fun createLockscreenSceneViewModel(): LockscreenUserActionsViewModel { + return LockscreenUserActionsViewModel( deviceEntryInteractor = kosmos.deviceEntryInteractor, communalInteractor = kosmos.communalInteractor, shadeInteractor = kosmos.shadeInteractor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModelTest.kt index ed7f96fb2b66..46b02e92a4f9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModelTest.kt @@ -37,7 +37,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneActionsViewModel +import com.android.systemui.shade.ui.viewmodel.notificationsShadeUserActionsViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -52,14 +52,14 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @EnableSceneContainer -class NotificationsShadeSceneActionsViewModelTest : SysuiTestCase() { +class NotificationsShadeUserActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val sceneInteractor by lazy { kosmos.sceneInteractor } private val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } - private val underTest by lazy { kosmos.notificationsShadeSceneActionsViewModel } + private val underTest by lazy { kosmos.notificationsShadeUserActionsViewModel } @Test fun upTransitionSceneKey_deviceLocked_lockscreen() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModelTest.kt index ba527d7ad2b8..32772d20a76a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModelTest.kt @@ -53,14 +53,14 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @EnableSceneContainer -class QuickSettingsShadeSceneActionsViewModelTest : SysuiTestCase() { +class QuickSettingsShadeUserActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val sceneInteractor = kosmos.sceneInteractor private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor - private val underTest by lazy { kosmos.quickSettingsShadeSceneActionsViewModel } + private val underTest by lazy { kosmos.quickSettingsShadeUserActionsViewModel } @Test fun upTransitionSceneKey_deviceLocked_lockscreen() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsUserActionsViewModelTest.kt index f26a9db56450..6986cf8ee7dc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsUserActionsViewModelTest.kt @@ -57,7 +57,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @RunWithLooper @EnableSceneContainer -class QuickSettingsSceneActionsViewModelTest : SysuiTestCase() { +class QuickSettingsUserActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -67,7 +67,7 @@ class QuickSettingsSceneActionsViewModelTest : SysuiTestCase() { private val sceneBackInteractor = kosmos.sceneBackInteractor private val sceneContainerStartable = kosmos.sceneContainerStartable - private lateinit var underTest: QuickSettingsSceneActionsViewModel + private lateinit var underTest: QuickSettingsUserActionsViewModel @Before fun setUp() { @@ -75,7 +75,7 @@ class QuickSettingsSceneActionsViewModelTest : SysuiTestCase() { sceneContainerStartable.start() underTest = - QuickSettingsSceneActionsViewModel( + QuickSettingsUserActionsViewModel( qsSceneAdapter = qsFlexiglassAdapter, sceneBackInteractor = sceneBackInteractor, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index f365afbfcc06..4f7c01358a7f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -49,7 +49,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic -import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneActionsViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenUserActionsViewModel import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest @@ -65,10 +65,10 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.ui.viewmodel.ShadeSceneActionsViewModel import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel -import com.android.systemui.shade.ui.viewmodel.shadeSceneActionsViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeUserActionsViewModel import com.android.systemui.shade.ui.viewmodel.shadeSceneContentViewModel +import com.android.systemui.shade.ui.viewmodel.shadeUserActionsViewModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository import com.android.systemui.telephony.data.repository.fakeTelephonyRepository @@ -145,8 +145,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel - private val lockscreenSceneActionsViewModel by lazy { - LockscreenSceneActionsViewModel( + private val mLockscreenUserActionsViewModel by lazy { + LockscreenUserActionsViewModel( deviceEntryInteractor = deviceEntryInteractor, communalInteractor = communalInteractor, shadeInteractor = kosmos.shadeInteractor, @@ -154,7 +154,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } private lateinit var shadeSceneContentViewModel: ShadeSceneContentViewModel - private lateinit var shadeSceneActionsViewModel: ShadeSceneActionsViewModel + private lateinit var mShadeUserActionsViewModel: ShadeUserActionsViewModel private val powerInteractor by lazy { kosmos.powerInteractor } @@ -191,14 +191,14 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel shadeSceneContentViewModel = kosmos.shadeSceneContentViewModel - shadeSceneActionsViewModel = kosmos.shadeSceneActionsViewModel + mShadeUserActionsViewModel = kosmos.shadeUserActionsViewModel val startable = kosmos.sceneContainerStartable startable.start() - lockscreenSceneActionsViewModel.activateIn(testScope) + mLockscreenUserActionsViewModel.activateIn(testScope) shadeSceneContentViewModel.activateIn(testScope) - shadeSceneActionsViewModel.activateIn(testScope) + mShadeUserActionsViewModel.activateIn(testScope) bouncerSceneContentViewModel.activateIn(testScope) sceneContainerViewModel.activateIn(testScope) @@ -229,7 +229,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun swipeUpOnLockscreen_enterCorrectPin_unlocksDevice() = testScope.runTest { - val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val actions by collectLastValue(mLockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) @@ -250,7 +250,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { testScope.runTest { setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) - val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val actions by collectLastValue(mLockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) @@ -262,7 +262,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { - val actions by collectLastValue(shadeSceneActionsViewModel.actions) + val actions by collectLastValue(mShadeUserActionsViewModel.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) assertCurrentScene(Scenes.Lockscreen) @@ -283,7 +283,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenDismissed_goesToGone() = testScope.runTest { - val actions by collectLastValue(shadeSceneActionsViewModel.actions) + val actions by collectLastValue(mShadeUserActionsViewModel.actions) val canSwipeToEnter by collectLastValue(deviceEntryInteractor.canSwipeToEnter) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) @@ -369,7 +369,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun swipeUpOnLockscreenWhileUnlocked_dismissesLockscreen() = testScope.runTest { unlockDevice() - val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val actions by collectLastValue(mLockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) @@ -392,7 +392,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun dismissingIme_whileOnPasswordBouncer_navigatesToLockscreen() = testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) - val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val actions by collectLastValue(mLockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) @@ -411,7 +411,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun bouncerActionButtonClick_opensEmergencyServicesDialer() = testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) - val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val actions by collectLastValue(mLockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) @@ -432,7 +432,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) startPhoneCall() - val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val actions by collectLastValue(mLockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModelTest.kt index b52627570246..03106eca1f63 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModelTest.kt @@ -43,17 +43,17 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @EnableSceneContainer -class GoneSceneActionsViewModelTest : SysuiTestCase() { +class GoneUserActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val shadeRepository by lazy { kosmos.shadeRepository } - private lateinit var underTest: GoneSceneActionsViewModel + private lateinit var underTest: GoneUserActionsViewModel @Before fun setUp() { underTest = - GoneSceneActionsViewModel( + GoneUserActionsViewModel( shadeInteractor = kosmos.shadeInteractor, ) underTest.activateIn(testScope) @@ -62,21 +62,21 @@ class GoneSceneActionsViewModelTest : SysuiTestCase() { @Test fun downTransitionKey_splitShadeEnabled_isGoneToSplitShade() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.actions) + val userActions by collectLastValue(underTest.actions) shadeRepository.setShadeLayoutWide(true) runCurrent() - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey) + assertThat(userActions?.get(Swipe(SwipeDirection.Down))?.transitionKey) .isEqualTo(ToSplitShade) } @Test fun downTransitionKey_splitShadeDisabled_isNull() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.actions) + val userActions by collectLastValue(underTest.actions) shadeRepository.setShadeLayoutWide(false) runCurrent() - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey).isNull() + assertThat(userActions?.get(Swipe(SwipeDirection.Down))?.transitionKey).isNull() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/UserActionsViewModelTest.kt index 900f2a464588..972afb58352d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/UserActionsViewModelTest.kt @@ -42,12 +42,12 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -class SceneActionsViewModelTest : SysuiTestCase() { +class UserActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val underTest = FakeSceneActionsViewModel() + private val underTest = FakeUserActionsViewModel() @Test fun actions_emptyBeforeActivation() = @@ -115,7 +115,7 @@ class SceneActionsViewModelTest : SysuiTestCase() { assertThat(actions).isEmpty() } - private class FakeSceneActionsViewModel : SceneActionsViewModel() { + private class FakeUserActionsViewModel : UserActionsViewModel() { val upstream = MutableStateFlow<Map<UserAction, UserActionResult>>(emptyMap()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt index a931e656c3c6..9f3e126ed1e8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt @@ -64,7 +64,7 @@ import org.junit.runner.RunWith @TestableLooper.RunWithLooper @EnableSceneContainer @DisableFlags(DualShade.FLAG_NAME) -class ShadeSceneActionsViewModelTest : SysuiTestCase() { +class ShadeUserActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -72,7 +72,7 @@ class ShadeSceneActionsViewModelTest : SysuiTestCase() { private val shadeRepository by lazy { kosmos.shadeRepository } private val qsSceneAdapter by lazy { kosmos.fakeQSSceneAdapter } - private val underTest: ShadeSceneActionsViewModel by lazy { kosmos.shadeSceneActionsViewModel } + private val underTest: ShadeUserActionsViewModel by lazy { kosmos.shadeUserActionsViewModel } @Before fun setUp() { diff --git a/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml b/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml index e06bfdc500da..368fe829cf9f 100644 --- a/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml +++ b/packages/SystemUI/res/layout/alert_dialog_button_bar_systemui.xml @@ -52,7 +52,7 @@ <Button android:id="@android:id/button1" style="?android:attr/buttonBarPositiveButtonStyle" - android:layout_marginStart="8dp" + android:layout_marginStart="@dimen/dialog_button_side_margin" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </com.android.internal.widget.ButtonBarLayout> diff --git a/packages/SystemUI/res/layout/screen_share_dialog.xml b/packages/SystemUI/res/layout/screen_share_dialog.xml index aa083ad9fdea..0533c7e3fc50 100644 --- a/packages/SystemUI/res/layout/screen_share_dialog.xml +++ b/packages/SystemUI/res/layout/screen_share_dialog.xml @@ -64,30 +64,27 @@ android:layout_height="wrap_content" android:text="@string/screenrecord_permission_dialog_warning_entire_screen" style="@style/TextAppearance.Dialog.Body.Message" - android:gravity="start"/> + android:gravity="start" + android:textAlignment="gravity"/> <!-- Buttons --> <com.android.internal.widget.ButtonBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:layout_marginTop="@dimen/screenrecord_buttons_margin_top"> + android:layout_marginTop="@dimen/screenrecord_buttons_margin_top" + android:gravity="end"> <Button android:id="@android:id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="0" android:text="@string/cancel" style="@style/Widget.Dialog.Button.BorderButton" /> - <Space - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1"/> <Button android:id="@android:id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="0" + android:layout_marginStart="@dimen/dialog_button_side_margin" android:text="@string/screenrecord_continue" style="@style/Widget.Dialog.Button" /> </com.android.internal.widget.ButtonBarLayout> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index e5750d278bfe..141d03599867 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1751,6 +1751,7 @@ <!-- System UI Dialog --> <dimen name="dialog_title_text_size">24sp</dimen> + <dimen name="dialog_button_side_margin">8dp</dimen> <!-- Internet panel related dimensions --> <dimen name="internet_dialog_list_max_height">662dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d3d757bcdb46..be74291e705a 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3699,6 +3699,22 @@ --> <string name="shortcut_helper_key_combinations_or_separator">or</string> + <!-- Keyboard touchpad tutorial scheduler--> + <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] --> + <string name="launch_keyboard_tutorial_notification_title">Navigate using your keyboard</string> + <!-- Notification text for launching keyboard tutorial [CHAR_LIMIT=100] --> + <string name="launch_keyboard_tutorial_notification_content">Learn keyboards shortcuts</string> + + <!-- Notification title for launching touchpad tutorial [CHAR_LIMIT=100] --> + <string name="launch_touchpad_tutorial_notification_title">Navigate using your touchpad</string> + <!-- Notification text for launching keyboard tutorial [CHAR_LIMIT=100] --> + <string name="launch_touchpad_tutorial_notification_content">Learn touchpad gestures</string> + + <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] --> + <string name="launch_keyboard_touchpad_tutorial_notification_title">Navigate using your keyboard and touchpad</string> + <!-- Notification text for launching keyboard tutorial [CHAR_LIMIT=100] --> + <string name="launch_keyboard_touchpad_tutorial_notification_content">Learn touchpad gestures, keyboards shortcuts, and more</string> + <!-- TOUCHPAD TUTORIAL--> <!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] --> <string name="touchpad_tutorial_back_gesture_button">Back gesture</string> diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerUserActionsViewModel.kt index 2d57e5b4f204..4fe6fc69e8be 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerUserActionsViewModel.kt @@ -22,7 +22,7 @@ import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.bouncer.domain.interactor.BouncerInteractor -import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.map @@ -31,11 +31,11 @@ import kotlinx.coroutines.flow.map * Models UI state for user actions that can lead to navigation to other scenes when showing the * bouncer scene. */ -class BouncerSceneActionsViewModel +class BouncerUserActionsViewModel @AssistedInject constructor( private val bouncerInteractor: BouncerInteractor, -) : SceneActionsViewModel() { +) : UserActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { bouncerInteractor.dismissDestination @@ -50,6 +50,6 @@ constructor( @AssistedFactory interface Factory { - fun create(): BouncerSceneActionsViewModel + fun create(): BouncerUserActionsViewModel } } diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt index 87eeebf333e9..3b2d77172393 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -24,8 +24,6 @@ import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLoggin import com.android.systemui.contextualeducation.GestureType import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.contextualeducation.GestureType.BACK -import com.android.systemui.contextualeducation.GestureType.HOME -import com.android.systemui.contextualeducation.GestureType.OVERVIEW import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.dagger.ContextualEducationModule.EduClock @@ -68,11 +66,9 @@ constructor( private val keyboardShortcutTriggered: Flow<GestureType> = conflatedCallbackFlow { val listener = KeyGestureEventListener { event -> + // Only store keyboard shortcut time for gestures providing keyboard education val shortcutType = when (event.keyGestureType) { - KeyGestureEvent.KEY_GESTURE_TYPE_BACK -> BACK - KeyGestureEvent.KEY_GESTURE_TYPE_HOME -> HOME - KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS -> OVERVIEW KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS else -> null } @@ -87,6 +83,7 @@ constructor( } override fun start() { + // Listen to back gesture model changes and trigger education if needed backgroundScope.launch { contextualEducationInteractor.backGestureModelFlow.collect { if (isUsageSessionExpired(it)) { @@ -98,6 +95,7 @@ constructor( } } + // Listen to touchpad connection changes and update the first connection time backgroundScope.launch { userInputDeviceRepository.isAnyTouchpadConnectedForUser.collect { if ( @@ -111,6 +109,7 @@ constructor( } } + // Listen to keyboard connection changes and update the first connection time backgroundScope.launch { userInputDeviceRepository.isAnyKeyboardConnectedForUser.collect { if ( @@ -124,6 +123,7 @@ constructor( } } + // Listen to keyboard shortcut triggered and update the last trigger time backgroundScope.launch { keyboardShortcutTriggered.collect { contextualEducationInteractor.updateShortcutTriggerTime(it) diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt index 3223433568b9..7821f6940da4 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt @@ -16,11 +16,25 @@ package com.android.systemui.education.domain.interactor +import android.os.SystemProperties +import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.education.dagger.ContextualEducationModule.EduClock +import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD +import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository +import java.time.Clock import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours +import kotlin.time.DurationUnit +import kotlin.time.toDuration import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch /** @@ -39,12 +53,29 @@ class KeyboardTouchpadEduStatsInteractorImpl @Inject constructor( @Background private val backgroundScope: CoroutineScope, - private val contextualEducationInteractor: ContextualEducationInteractor + private val contextualEducationInteractor: ContextualEducationInteractor, + private val inputDeviceRepository: UserInputDeviceRepository, + private val tutorialRepository: TutorialSchedulerRepository, + @EduClock private val clock: Clock, ) : KeyboardTouchpadEduStatsInteractor { + companion object { + val initialDelayDuration: Duration + get() = + SystemProperties.getLong( + "persist.contextual_edu.initial_delay_sec", + /* defaultValue= */ 72.hours.inWholeSeconds + ) + .toDuration(DurationUnit.SECONDS) + } + override fun incrementSignalCount(gestureType: GestureType) { - // Todo: check if keyboard/touchpad is connected before update - backgroundScope.launch { contextualEducationInteractor.incrementSignalCount(gestureType) } + backgroundScope.launch { + val targetDevice = getTargetDevice(gestureType) + if (isTargetDeviceConnected(targetDevice) && hasInitialDelayElapsed(targetDevice)) { + contextualEducationInteractor.incrementSignalCount(gestureType) + } + } } override fun updateShortcutTriggerTime(gestureType: GestureType) { @@ -52,4 +83,31 @@ constructor( contextualEducationInteractor.updateShortcutTriggerTime(gestureType) } } + + private suspend fun isTargetDeviceConnected(deviceType: DeviceType): Boolean { + if (deviceType == KEYBOARD) { + return inputDeviceRepository.isAnyKeyboardConnectedForUser.first().isConnected + } else if (deviceType == TOUCHPAD) { + return inputDeviceRepository.isAnyTouchpadConnectedForUser.first().isConnected + } + return false + } + + /** + * Keyboard shortcut education would be provided for All Apps. Touchpad gesture education would + * be provided for the rest of the gesture types (i.e. Home, Overview, Back). This method maps + * gesture to its target education device. + */ + private fun getTargetDevice(gestureType: GestureType) = + when (gestureType) { + ALL_APPS -> KEYBOARD + else -> TOUCHPAD + } + + private suspend fun hasInitialDelayElapsed(deviceType: DeviceType): Boolean { + val oobeLaunchTime = tutorialRepository.launchTime(deviceType) ?: return false + return clock + .instant() + .isAfter(oobeLaunchTime.plusSeconds(initialDelayDuration.inWholeSeconds)) + } } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt index e8e1dd4c85d0..7ecacdc7cf16 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt @@ -18,20 +18,20 @@ package com.android.systemui.inputdevice.tutorial import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor +import com.android.systemui.inputdevice.tutorial.ui.TutorialNotificationCoordinator import com.android.systemui.shared.Flags.newTouchpadGesturesTutorial import dagger.Lazy import javax.inject.Inject -/** A [CoreStartable] to launch a scheduler for keyboard and touchpad education */ +/** A [CoreStartable] to launch a scheduler for keyboard and touchpad tutorial notification */ @SysUISingleton class KeyboardTouchpadTutorialCoreStartable @Inject -constructor(private val tutorialSchedulerInteractor: Lazy<TutorialSchedulerInteractor>) : +constructor(private val tutorialNotificationCoordinator: Lazy<TutorialNotificationCoordinator>) : CoreStartable { override fun start() { if (newTouchpadGesturesTutorial()) { - tutorialSchedulerInteractor.get().start() + tutorialNotificationCoordinator.get().start() } } } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt index a8d7dad42a93..cfc913fbc89b 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt @@ -17,9 +17,7 @@ package com.android.systemui.inputdevice.tutorial.domain.interactor import android.os.SystemProperties -import android.util.Log import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD @@ -31,23 +29,22 @@ import java.time.Instant import javax.inject.Inject import kotlin.time.Duration.Companion.hours import kotlin.time.toKotlinDuration -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.launch /** - * When the first time a keyboard or touchpad is connected, wait for [LAUNCH_DELAY], then launch the - * tutorial as soon as there's a connected device + * When the first time a keyboard or touchpad is connected, wait for [LAUNCH_DELAY], and as soon as + * there's a connected device, show a notification to launch the tutorial. */ @SysUISingleton class TutorialSchedulerInteractor @Inject constructor( - @Background private val backgroundScope: CoroutineScope, keyboardRepository: KeyboardRepository, touchpadRepository: TouchpadRepository, private val repo: TutorialSchedulerRepository @@ -58,17 +55,6 @@ constructor( TOUCHPAD to touchpadRepository.isAnyTouchpadConnected ) - fun start() { - backgroundScope.launch { - // Merging two flows to ensure that launch tutorial is launched consecutively in order - // to avoid race condition - merge(touchpadScheduleFlow, keyboardScheduleFlow).collect { - val tutorialType = resolveTutorialType(it) - launchTutorial(tutorialType) - } - } - } - private val touchpadScheduleFlow = flow { if (!repo.isLaunched(TOUCHPAD)) { schedule(TOUCHPAD) @@ -95,14 +81,19 @@ constructor( private suspend fun waitForDeviceConnection(deviceType: DeviceType) = isAnyDeviceConnected[deviceType]!!.filter { it }.first() - private suspend fun launchTutorial(tutorialType: TutorialType) { - if (tutorialType == TutorialType.KEYBOARD || tutorialType == TutorialType.BOTH) - repo.updateLaunchTime(KEYBOARD, Instant.now()) - if (tutorialType == TutorialType.TOUCHPAD || tutorialType == TutorialType.BOTH) - repo.updateLaunchTime(TOUCHPAD, Instant.now()) - // TODO: launch tutorial - Log.d(TAG, "Launch tutorial for $tutorialType") - } + // Merging two flows ensures that tutorial is launched consecutively to avoid race condition + val tutorials: Flow<TutorialType> = + merge(touchpadScheduleFlow, keyboardScheduleFlow).map { + val tutorialType = resolveTutorialType(it) + + // TODO: notifying time is not oobe launching time - move these updates into oobe + if (tutorialType == TutorialType.KEYBOARD || tutorialType == TutorialType.BOTH) + repo.updateLaunchTime(KEYBOARD, Instant.now()) + if (tutorialType == TutorialType.TOUCHPAD || tutorialType == TutorialType.BOTH) + repo.updateLaunchTime(TOUCHPAD, Instant.now()) + + tutorialType + } private suspend fun resolveTutorialType(deviceType: DeviceType): TutorialType { // Resolve the type of tutorial depending on which device are connected when the tutorial is diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt new file mode 100644 index 000000000000..5d9dda3899cd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt @@ -0,0 +1,146 @@ +/* + * 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.inputdevice.tutorial.ui + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.app.NotificationCompat +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor +import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.Companion.TAG +import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.TutorialType +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_BOTH +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_TOUCHPAD +import com.android.systemui.res.R +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** When the scheduler is due, show a notification to launch tutorial */ +@SysUISingleton +class TutorialNotificationCoordinator +@Inject +constructor( + @Background private val backgroundScope: CoroutineScope, + @Application private val context: Context, + private val tutorialSchedulerInteractor: TutorialSchedulerInteractor, + private val notificationManager: NotificationManager +) { + fun start() { + backgroundScope.launch { + tutorialSchedulerInteractor.tutorials.collect { showNotification(it) } + } + } + + // By sharing the same tag and id, we update the content of existing notification instead of + // creating multiple notifications + private fun showNotification(tutorialType: TutorialType) { + if (tutorialType == TutorialType.NONE) return + + if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) + createNotificationChannel() + + // Replace "System UI" app name with "Android System" + val extras = Bundle() + extras.putString( + Notification.EXTRA_SUBSTITUTE_APP_NAME, + context.getString(com.android.internal.R.string.android_system_label) + ) + + val info = getNotificationInfo(tutorialType)!! + val notification = + NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_settings) + .setContentTitle(info.title) + .setContentText(info.text) + .setContentIntent(createPendingIntent(info.type)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + .addExtras(extras) + .build() + + notificationManager.notify(TAG, NOTIFICATION_ID, notification) + } + + private fun createNotificationChannel() { + val channel = + NotificationChannel( + CHANNEL_ID, + context.getString(com.android.internal.R.string.android_system_label), + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManager.createNotificationChannel(channel) + } + + private fun createPendingIntent(tutorialType: String): PendingIntent { + val intent = + Intent(context, KeyboardTouchpadTutorialActivity::class.java).apply { + putExtra(INTENT_TUTORIAL_TYPE_KEY, tutorialType) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + return PendingIntent.getActivity( + context, + /* requestCode= */ 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + } + + private data class NotificationInfo(val title: String, val text: String, val type: String) + + private fun getNotificationInfo(tutorialType: TutorialType): NotificationInfo? = + when (tutorialType) { + TutorialType.KEYBOARD -> + NotificationInfo( + context.getString(R.string.launch_keyboard_tutorial_notification_title), + context.getString(R.string.launch_keyboard_tutorial_notification_content), + INTENT_TUTORIAL_TYPE_KEYBOARD + ) + TutorialType.TOUCHPAD -> + NotificationInfo( + context.getString(R.string.launch_touchpad_tutorial_notification_title), + context.getString(R.string.launch_touchpad_tutorial_notification_content), + INTENT_TUTORIAL_TYPE_TOUCHPAD + ) + TutorialType.BOTH -> + NotificationInfo( + context.getString( + R.string.launch_keyboard_touchpad_tutorial_notification_title + ), + context.getString( + R.string.launch_keyboard_touchpad_tutorial_notification_content + ), + INTENT_TUTORIAL_TYPE_BOTH + ) + TutorialType.NONE -> null + } + + companion object { + private const val CHANNEL_ID = "TutorialSchedulerNotificationChannel" + private const val NOTIFICATION_ID = 5566 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt index 8debe7975197..1adc285e6bb5 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt @@ -54,6 +54,7 @@ constructor( const val INTENT_TUTORIAL_TYPE_KEY = "tutorial_type" const val INTENT_TUTORIAL_TYPE_TOUCHPAD = "touchpad" const val INTENT_TUTORIAL_TYPE_KEYBOARD = "keyboard" + const val INTENT_TUTORIAL_TYPE_BOTH = "both" } private val vm by diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt index 2819e617629d..dd47678e5b36 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt @@ -28,7 +28,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade -import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.util.kotlin.filterValuesNotNull @@ -40,13 +40,13 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf /** Models UI state and handles user input for the lockscreen scene. */ -class LockscreenSceneActionsViewModel +class LockscreenUserActionsViewModel @AssistedInject constructor( private val deviceEntryInteractor: DeviceEntryInteractor, private val communalInteractor: CommunalInteractor, private val shadeInteractor: ShadeInteractor, -) : SceneActionsViewModel() { +) : UserActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { shadeInteractor.isShadeTouchable @@ -119,6 +119,6 @@ constructor( @AssistedFactory interface Factory { - fun create(): LockscreenSceneActionsViewModel + fun create(): LockscreenUserActionsViewModel } } diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt index 6ef83e262ac8..b6868c172a9f 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt @@ -21,13 +21,13 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject /** Models the UI state for the user actions for navigating to other scenes or overlays. */ class NotificationsShadeOverlayActionsViewModel @AssistedInject constructor() : - SceneActionsViewModel() { + UserActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { setActions( diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt index 572a0caf49f2..a5c07bc2fdbf 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt @@ -21,15 +21,15 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject /** * Models the UI state for the user actions that the user can perform to navigate to other scenes. */ -class NotificationsShadeSceneActionsViewModel @AssistedInject constructor() : - SceneActionsViewModel() { +class NotificationsShadeUserActionsViewModel @AssistedInject constructor() : + UserActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { setActions( @@ -42,6 +42,6 @@ class NotificationsShadeSceneActionsViewModel @AssistedInject constructor() : @AssistedFactory interface Factory { - fun create(): NotificationsShadeSceneActionsViewModel + fun create(): NotificationsShadeUserActionsViewModel } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt index a264f5142293..f77386dbe91b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.launch /** * Models UI state needed for rendering the content of the quick settings scene. * - * Different from [QuickSettingsSceneActionsViewModel] that models the UI state needed to figure out + * Different from [QuickSettingsUserActionsViewModel] that models the UI state needed to figure out * which user actions can trigger navigation to other scenes. */ class QuickSettingsSceneContentViewModel diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt index 9538392b845f..61c4c8c0de86 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt @@ -21,13 +21,13 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject /** Models the UI state for the user actions for navigating to other scenes or overlays. */ class QuickSettingsShadeOverlayActionsViewModel @AssistedInject constructor() : - SceneActionsViewModel() { + UserActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { setActions( diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt index 518582843401..d01b33b7be59 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt @@ -22,7 +22,7 @@ import dagger.assisted.AssistedInject /** * Models UI state used to render the content of the quick settings shade scene. * - * Different from [QuickSettingsShadeSceneActionsViewModel], which only models user actions that can + * Different from [QuickSettingsShadeUserActionsViewModel], which only models user actions that can * be performed to navigate to other scenes. */ class QuickSettingsShadeSceneContentViewModel diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt index 9690aabdba81..d3dc302d44ca 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt @@ -21,7 +21,7 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.map @@ -32,11 +32,11 @@ import kotlinx.coroutines.flow.map * Different from the [QuickSettingsShadeSceneContentViewModel] which models the _content_ of the * scene. */ -class QuickSettingsShadeSceneActionsViewModel +class QuickSettingsShadeUserActionsViewModel @AssistedInject constructor( val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, -) : SceneActionsViewModel() { +) : UserActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { quickSettingsContainerViewModel.editModeViewModel.isEditing @@ -53,6 +53,6 @@ constructor( @AssistedFactory interface Factory { - fun create(): QuickSettingsShadeSceneActionsViewModel + fun create(): QuickSettingsShadeUserActionsViewModel } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsUserActionsViewModel.kt index 2bb5dc66bc16..54e5caca107c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsUserActionsViewModel.kt @@ -27,7 +27,7 @@ import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.domain.interactor.SceneBackInteractor import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow @@ -42,12 +42,12 @@ import kotlinx.coroutines.flow.map * Different from [QuickSettingsSceneContentViewModel] that models UI state needed for rendering the * content of the quick settings scene. */ -class QuickSettingsSceneActionsViewModel +class QuickSettingsUserActionsViewModel @AssistedInject constructor( private val qsSceneAdapter: QSSceneAdapter, sceneBackInteractor: SceneBackInteractor, -) : SceneActionsViewModel() { +) : UserActionsViewModel() { private val backScene: Flow<SceneKey> = sceneBackInteractor.backScene @@ -82,6 +82,6 @@ constructor( @AssistedFactory interface Factory { - fun create(): QuickSettingsSceneActionsViewModel + fun create(): QuickSettingsUserActionsViewModel } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt index 7b0e7f4ded58..ea4122a563ab 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt @@ -29,11 +29,11 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.map -class GoneSceneActionsViewModel +class GoneUserActionsViewModel @AssistedInject constructor( private val shadeInteractor: ShadeInteractor, -) : SceneActionsViewModel() { +) : UserActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { shadeInteractor.shadeMode @@ -69,6 +69,6 @@ constructor( @AssistedFactory interface Factory { - fun create(): GoneSceneActionsViewModel + fun create(): GoneUserActionsViewModel } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/UserActionsViewModel.kt index 076613005959..57628d0f3f40 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/UserActionsViewModel.kt @@ -25,15 +25,13 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow /** - * Base class for view-models that need to keep a map of scene actions (also known as "destination - * scenes") up-to-date. + * Base class for view-models that need to keep a map of user actions up-to-date. * * Subclasses need only to override [hydrateActions], suspending forever if they need; they don't * need to worry about resetting the value of [actions] when the view-model is deactivated/canceled, * this base class takes care of it. */ -// TODO(b/363206563): Rename to UserActionsViewModel. -abstract class SceneActionsViewModel : ExclusiveActivatable() { +abstract class UserActionsViewModel : ExclusiveActivatable() { private val _actions = MutableStateFlow<Map<UserAction, UserActionResult>>(emptyMap()) /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt index 7c707592f5ab..ce4c081358ba 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt @@ -43,7 +43,7 @@ import kotlinx.coroutines.flow.asStateFlow /** * Models UI state used to render the content of the shade scene. * - * Different from [ShadeSceneActionsViewModel], which only models user actions that can be performed + * Different from [ShadeUserActionsViewModel], which only models user actions that can be performed * to navigate to other scenes. */ class ShadeSceneContentViewModel diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt index ab719132b93e..f8a850a357f1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt @@ -24,7 +24,7 @@ import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade -import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode import dagger.assisted.AssistedFactory @@ -36,12 +36,12 @@ import kotlinx.coroutines.flow.combine * * Different from the [ShadeSceneContentViewModel] which models the _content_ of the scene. */ -class ShadeSceneActionsViewModel +class ShadeUserActionsViewModel @AssistedInject constructor( private val qsSceneAdapter: QSSceneAdapter, private val shadeInteractor: ShadeInteractor, -) : SceneActionsViewModel() { +) : UserActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { combine( @@ -71,6 +71,6 @@ constructor( @AssistedFactory interface Factory { - fun create(): ShadeSceneActionsViewModel + fun create(): ShadeUserActionsViewModel } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 28effe909521..8934d8f8a954 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -1280,6 +1280,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final class Receiver extends BroadcastReceiver { + private static final int STREAM_UNKNOWN = -1; + public void init() { final IntentFilter filter = new IntentFilter(); filter.addAction(AudioManager.VOLUME_CHANGED_ACTION); @@ -1301,30 +1303,39 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa final String action = intent.getAction(); boolean changed = false; if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) { - final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, + STREAM_UNKNOWN); final int level = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1); final int oldLevel = intent .getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, -1); if (D.BUG) Log.d(TAG, "onReceive VOLUME_CHANGED_ACTION stream=" + stream + " level=" + level + " oldLevel=" + oldLevel); - changed = updateStreamLevelW(stream, level); + if (stream != STREAM_UNKNOWN) { + changed = updateStreamLevelW(stream, level); + } } else if (action.equals(AudioManager.STREAM_DEVICES_CHANGED_ACTION)) { - final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, + STREAM_UNKNOWN); final int devices = intent .getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_DEVICES, -1); final int oldDevices = intent .getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_DEVICES, -1); if (D.BUG) Log.d(TAG, "onReceive STREAM_DEVICES_CHANGED_ACTION stream=" + stream + " devices=" + devices + " oldDevices=" + oldDevices); - changed = checkRoutedToBluetoothW(stream); - changed |= onVolumeChangedW(stream, 0); + if (stream != STREAM_UNKNOWN) { + changed |= checkRoutedToBluetoothW(stream); + changed |= onVolumeChangedW(stream, 0); + } } else if (action.equals(AudioManager.STREAM_MUTE_CHANGED_ACTION)) { - final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, + STREAM_UNKNOWN); final boolean muted = intent .getBooleanExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, false); if (D.BUG) Log.d(TAG, "onReceive STREAM_MUTE_CHANGED_ACTION stream=" + stream + " muted=" + muted); - changed = updateStreamMuteW(stream, muted); + if (stream != STREAM_UNKNOWN) { + changed = updateStreamMuteW(stream, muted); + } } else if (action.equals(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED)) { if (D.BUG) Log.d(TAG, "onReceive ACTION_EFFECTS_SUPPRESSOR_CHANGED"); changed = updateEffectsSuppressorW(mNoMan.getEffectsSuppressor()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt new file mode 100644 index 000000000000..945f95385db2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt @@ -0,0 +1,158 @@ +/* + * 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.inputdevice.tutorial.domain.interactor + +import android.app.Notification +import android.app.NotificationManager +import androidx.annotation.StringRes +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository +import com.android.systemui.inputdevice.tutorial.ui.TutorialNotificationCoordinator +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.touchpad.data.repository.FakeTouchpadRepository +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.hours +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class TutorialNotificationCoordinatorTest : SysuiTestCase() { + + private lateinit var underTest: TutorialNotificationCoordinator + private val kosmos = Kosmos() + private val testScope = kosmos.testScope + private val keyboardRepository = FakeKeyboardRepository() + private val touchpadRepository = FakeTouchpadRepository() + private lateinit var dataStoreScope: CoroutineScope + private lateinit var repository: TutorialSchedulerRepository + @Mock private lateinit var notificationManager: NotificationManager + @Captor private lateinit var notificationCaptor: ArgumentCaptor<Notification> + @get:Rule val rule = MockitoJUnit.rule() + + @Before + fun setup() { + dataStoreScope = CoroutineScope(Dispatchers.Unconfined) + repository = + TutorialSchedulerRepository( + context, + dataStoreScope, + dataStoreName = "TutorialNotificationCoordinatorTest" + ) + val interactor = + TutorialSchedulerInteractor(keyboardRepository, touchpadRepository, repository) + underTest = + TutorialNotificationCoordinator( + testScope.backgroundScope, + context, + interactor, + notificationManager + ) + notificationCaptor = ArgumentCaptor.forClass(Notification::class.java) + underTest.start() + } + + @After + fun clear() { + runBlocking { repository.clearDataStore() } + dataStoreScope.cancel() + } + + @Test + fun showKeyboardNotification() = + testScope.runTest { + keyboardRepository.setIsAnyKeyboardConnected(true) + advanceTimeBy(LAUNCH_DELAY) + verifyNotification( + R.string.launch_keyboard_tutorial_notification_title, + R.string.launch_keyboard_tutorial_notification_content + ) + } + + @Test + fun showTouchpadNotification() = + testScope.runTest { + touchpadRepository.setIsAnyTouchpadConnected(true) + advanceTimeBy(LAUNCH_DELAY) + verifyNotification( + R.string.launch_touchpad_tutorial_notification_title, + R.string.launch_touchpad_tutorial_notification_content + ) + } + + @Test + fun showKeyboardTouchpadNotification() = + testScope.runTest { + keyboardRepository.setIsAnyKeyboardConnected(true) + touchpadRepository.setIsAnyTouchpadConnected(true) + advanceTimeBy(LAUNCH_DELAY) + verifyNotification( + R.string.launch_keyboard_touchpad_tutorial_notification_title, + R.string.launch_keyboard_touchpad_tutorial_notification_content + ) + } + + @Test + fun doNotShowNotification() = + testScope.runTest { + advanceTimeBy(LAUNCH_DELAY) + verify(notificationManager, never()).notify(eq(TAG), eq(NOTIFICATION_ID), any()) + } + + private fun verifyNotification(@StringRes titleResId: Int, @StringRes contentResId: Int) { + verify(notificationManager) + .notify(eq(TAG), eq(NOTIFICATION_ID), notificationCaptor.capture()) + val notification = notificationCaptor.value + val actualTitle = notification.getString(Notification.EXTRA_TITLE) + val actualContent = notification.getString(Notification.EXTRA_TEXT) + assertThat(actualTitle).isEqualTo(context.getString(titleResId)) + assertThat(actualContent).isEqualTo(context.getString(contentResId)) + } + + private fun Notification.getString(key: String): String = + this.extras?.getCharSequence(key).toString() + + companion object { + private const val TAG = "TutorialSchedulerInteractor" + private const val NOTIFICATION_ID = 5566 + private val LAUNCH_DELAY = 72.hours + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt index 432f7af7c29d..650f9dc7f104 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt @@ -32,6 +32,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest @@ -63,13 +65,7 @@ class TutorialSchedulerInteractorTest : SysuiTestCase() { dataStoreName = "TutorialSchedulerInteractorTest" ) underTest = - TutorialSchedulerInteractor( - testScope.backgroundScope, - keyboardRepository, - touchpadRepository, - schedulerRepository - ) - underTest.start() + TutorialSchedulerInteractor(keyboardRepository, touchpadRepository, schedulerRepository) } @After @@ -81,80 +77,90 @@ class TutorialSchedulerInteractorTest : SysuiTestCase() { @Test fun connectKeyboard_delayElapse_launchForKeyboard() = testScope.runTest { + launchAndAssert(TutorialType.KEYBOARD) + keyboardRepository.setIsAnyKeyboardConnected(true) advanceTimeBy(LAUNCH_DELAY) - assertLaunch(TutorialType.KEYBOARD) } @Test fun connectBothDevices_delayElapse_launchForBoth() = testScope.runTest { + launchAndAssert(TutorialType.BOTH) + keyboardRepository.setIsAnyKeyboardConnected(true) touchpadRepository.setIsAnyTouchpadConnected(true) advanceTimeBy(LAUNCH_DELAY) - assertLaunch(TutorialType.BOTH) } @Test fun connectBothDevice_delayNotElapse_launchNothing() = testScope.runTest { + launchAndAssert(TutorialType.NONE) + keyboardRepository.setIsAnyKeyboardConnected(true) touchpadRepository.setIsAnyTouchpadConnected(true) advanceTimeBy(A_SHORT_PERIOD_OF_TIME) - assertLaunch(TutorialType.NONE) } @Test fun nothingConnect_delayElapse_launchNothing() = testScope.runTest { + launchAndAssert(TutorialType.NONE) + keyboardRepository.setIsAnyKeyboardConnected(false) touchpadRepository.setIsAnyTouchpadConnected(false) advanceTimeBy(LAUNCH_DELAY) - assertLaunch(TutorialType.NONE) } @Test fun connectKeyboard_thenTouchpad_delayElapse_launchForBoth() = testScope.runTest { + launchAndAssert(TutorialType.BOTH) + keyboardRepository.setIsAnyKeyboardConnected(true) advanceTimeBy(A_SHORT_PERIOD_OF_TIME) touchpadRepository.setIsAnyTouchpadConnected(true) advanceTimeBy(REMAINING_TIME) - assertLaunch(TutorialType.BOTH) } @Test fun connectKeyboard_thenTouchpad_removeKeyboard_delayElapse_launchNothing() = testScope.runTest { + launchAndAssert(TutorialType.NONE) + keyboardRepository.setIsAnyKeyboardConnected(true) advanceTimeBy(A_SHORT_PERIOD_OF_TIME) touchpadRepository.setIsAnyTouchpadConnected(true) keyboardRepository.setIsAnyKeyboardConnected(false) advanceTimeBy(REMAINING_TIME) - assertLaunch(TutorialType.NONE) } - // TODO: likely to be changed after we update TutorialSchedulerInteractor.launchTutorial - private suspend fun assertLaunch(tutorialType: TutorialType) { - when (tutorialType) { - TutorialType.KEYBOARD -> { - assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isTrue() - assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isFalse() - } - TutorialType.TOUCHPAD -> { - assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isFalse() - assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isTrue() - } - TutorialType.BOTH -> { - assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isTrue() - assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isTrue() - } - TutorialType.NONE -> { - assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isFalse() - assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isFalse() + private suspend fun launchAndAssert(expectedTutorial: TutorialType) = + testScope.backgroundScope.launch { + val actualTutorial = underTest.tutorials.first() + assertThat(actualTutorial).isEqualTo(expectedTutorial) + + // TODO: need to update after we move launch into the tutorial + when (expectedTutorial) { + TutorialType.KEYBOARD -> { + assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isTrue() + assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isFalse() + } + TutorialType.TOUCHPAD -> { + assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isFalse() + assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isTrue() + } + TutorialType.BOTH -> { + assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isTrue() + assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isTrue() + } + TutorialType.NONE -> { + assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isFalse() + assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isFalse() + } } } - } companion object { private val LAUNCH_DELAY = 72.hours diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java index a0d231b8bb6f..60a185537b0d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java @@ -51,6 +51,7 @@ import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.service.notification.StatusBarNotification; import android.view.ViewGroup; +import android.widget.ImageView; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -431,6 +432,32 @@ public class StatusBarIconViewTest extends SysuiTestCase { mIconView.getIconScale(), 0.01f); } + @Test + @EnableFlags({Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS}) + public void set_iconThatWantsFixedSpace_setsScaleType() { + mIconView.setScaleType(ImageView.ScaleType.FIT_START); + StatusBarIcon icon = new StatusBarIcon(UserHandle.ALL, "mockPackage", + Icon.createWithResource(mContext, R.drawable.ic_android), 0, 0, "", + StatusBarIcon.Type.SystemIcon, StatusBarIcon.Shape.FIXED_SPACE); + + mIconView.set(icon); + + assertThat(mIconView.getScaleType()).isEqualTo(ImageView.ScaleType.FIT_CENTER); + } + + @Test + @EnableFlags({Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS}) + public void set_iconWithOtherShape_keepsScaleType() { + mIconView.setScaleType(ImageView.ScaleType.FIT_START); + StatusBarIcon icon = new StatusBarIcon(UserHandle.ALL, "mockPackage", + Icon.createWithResource(mContext, R.drawable.ic_android), 0, 0, "", + StatusBarIcon.Type.SystemIcon, StatusBarIcon.Shape.WRAP_CONTENT); + + mIconView.set(icon); + + assertThat(mIconView.getScaleType()).isEqualTo(ImageView.ScaleType.FIT_START); + } + private static StatusBarNotification getMockSbn() { StatusBarNotification sbn = mock(StatusBarNotification.class); when(sbn.getNotification()).thenReturn(mock(Notification.class)); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt index 171be97bf964..649e4e8a6f7e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt @@ -33,16 +33,16 @@ import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.StateFlow -val Kosmos.bouncerSceneActionsViewModel by Fixture { - BouncerSceneActionsViewModel( +val Kosmos.bouncerUserActionsViewModel by Fixture { + BouncerUserActionsViewModel( bouncerInteractor = bouncerInteractor, ) } -val Kosmos.bouncerSceneActionsViewModelFactory by Fixture { - object : BouncerSceneActionsViewModel.Factory { - override fun create(): BouncerSceneActionsViewModel { - return bouncerSceneActionsViewModel +val Kosmos.bouncerUserActionsViewModelFactory by Fixture { + object : BouncerUserActionsViewModel.Factory { + override fun create(): BouncerUserActionsViewModel { + return bouncerUserActionsViewModel } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt index 811c6533c656..7ccacb66e124 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.education.domain.interactor import android.hardware.input.InputManager import com.android.systemui.education.data.repository.fakeEduClock import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository +import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository import com.android.systemui.keyboard.data.repository.keyboardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher @@ -50,6 +51,12 @@ var Kosmos.keyboardTouchpadEduStatsInteractor by Kosmos.Fixture { KeyboardTouchpadEduStatsInteractorImpl( backgroundScope = testScope.backgroundScope, - contextualEducationInteractor = contextualEducationInteractor + contextualEducationInteractor = contextualEducationInteractor, + inputDeviceRepository = mockUserInputDeviceRepository, + tutorialRepository = mockTutorialSchedulerRepository, + clock = fakeEduClock ) } + +var mockUserInputDeviceRepository = mock<UserInputDeviceRepository>() +var mockTutorialSchedulerRepository = mock<TutorialSchedulerRepository>() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModelKosmos.kt index 128a7fcbab25..06592b1ea3ed 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModelKosmos.kt @@ -18,9 +18,9 @@ package com.android.systemui.qs.ui.viewmodel import com.android.systemui.kosmos.Kosmos -val Kosmos.quickSettingsShadeSceneActionsViewModel: QuickSettingsShadeSceneActionsViewModel by +val Kosmos.quickSettingsShadeUserActionsViewModel: QuickSettingsShadeUserActionsViewModel by Kosmos.Fixture { - QuickSettingsShadeSceneActionsViewModel( + QuickSettingsShadeUserActionsViewModel( quickSettingsContainerViewModel = quickSettingsContainerViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeUserActionsViewModelKosmos.kt index f792ab95e8ac..6345c4076412 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeUserActionsViewModelKosmos.kt @@ -18,7 +18,7 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneActionsViewModel +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeUserActionsViewModel -val Kosmos.notificationsShadeSceneActionsViewModel: - NotificationsShadeSceneActionsViewModel by Fixture { NotificationsShadeSceneActionsViewModel() } +val Kosmos.notificationsShadeUserActionsViewModel: + NotificationsShadeUserActionsViewModel by Fixture { NotificationsShadeUserActionsViewModel() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt index 2387aa856fe6..48c5121c71c1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt @@ -21,8 +21,8 @@ import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.qs.ui.adapter.qsSceneAdapter import com.android.systemui.shade.domain.interactor.shadeInteractor -val Kosmos.shadeSceneActionsViewModel: ShadeSceneActionsViewModel by Fixture { - ShadeSceneActionsViewModel( +val Kosmos.shadeUserActionsViewModel: ShadeUserActionsViewModel by Fixture { + ShadeUserActionsViewModel( qsSceneAdapter = qsSceneAdapter, shadeInteractor = shadeInteractor, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt index 1fa6c3f2327b..888351f0a882 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt @@ -33,6 +33,12 @@ import kotlinx.coroutines.flow.update class FakeAudioRepository : AudioRepository { + private val unMutableStreams = + setOf( + AudioManager.STREAM_VOICE_CALL, + AudioManager.STREAM_ALARM, + ) + private val mutableMode = MutableStateFlow(AudioManager.MODE_NORMAL) override val mode: StateFlow<Int> = mutableMode.asStateFlow() @@ -73,7 +79,7 @@ class FakeAudioRepository : AudioRepository { volume = 0, minVolume = 0, maxVolume = 10, - isAffectedByMute = false, + isAffectedByMute = audioStream.value !in unMutableStreams, isAffectedByRingerMode = false, isMuted = false, ) diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java index d16a66522e51..1b2447e2c58a 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -26,6 +26,7 @@ import android.annotation.MainThread; import android.annotation.NonNull; import android.content.Context; import android.graphics.Region; +import android.hardware.input.InputManager; import android.os.Looper; import android.os.PowerManager; import android.os.SystemClock; @@ -57,6 +58,7 @@ import com.android.server.policy.WindowManagerPolicy; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Objects; import java.util.StringJoiner; /** @@ -748,6 +750,7 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo if ((mEnabledFeatures & FLAG_FEATURE_MOUSE_KEYS) != 0) { mMouseKeysInterceptor = new MouseKeysInterceptor(mAms, + Objects.requireNonNull(mContext.getSystemService(InputManager.class)), Looper.myLooper(), Display.DEFAULT_DISPLAY); addFirstEventHandler(Display.DEFAULT_DISPLAY, mMouseKeysInterceptor); diff --git a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java index 2ce5c2bc3790..54368ca9c03e 100644 --- a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java +++ b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java @@ -23,6 +23,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; +import android.hardware.input.InputManager; import android.hardware.input.VirtualMouse; import android.hardware.input.VirtualMouseButtonEvent; import android.hardware.input.VirtualMouseConfig; @@ -34,8 +35,11 @@ import android.os.Message; import android.util.Log; import android.util.Slog; import android.util.SparseArray; +import android.view.InputDevice; import android.view.KeyEvent; +import androidx.annotation.VisibleForTesting; + import com.android.server.LocalServices; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; @@ -60,7 +64,7 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal; * mouse keys of each physical keyboard will control a single (global) mouse pointer. */ public class MouseKeysInterceptor extends BaseEventStreamTransformation - implements Handler.Callback { + implements Handler.Callback, InputManager.InputDeviceListener { private static final String LOG_TAG = "MouseKeysInterceptor"; // To enable these logs, run: 'adb shell setprop log.tag.MouseKeysInterceptor DEBUG' @@ -77,10 +81,19 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation private final AccessibilityManagerService mAms; private final Handler mHandler; + private final InputManager mInputManager; /** Thread to wait for virtual mouse creation to complete */ private final Thread mCreateVirtualMouseThread; + /** + * Map of device IDs to a map of key codes to their corresponding {@link MouseKeyEvent} values. + * To ensure thread safety for the map, all access and modification of the map + * should happen on the same thread, i.e., on the handler thread. + */ + private final SparseArray<SparseArray<MouseKeyEvent>> mDeviceKeyCodeMap = + new SparseArray<>(); + VirtualDeviceManager.VirtualDevice mVirtualDevice = null; private VirtualMouse mVirtualMouse = null; @@ -102,6 +115,21 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation /** Whether scroll toggle is on */ private boolean mScrollToggleOn = false; + /** The ID of the input device that is currently active */ + private int mActiveInputDeviceId = 0; + + /** + * Enum representing different types of mouse key events, each associated with a specific + * key code. + * + * <p> These events correspond to various mouse actions such as directional movements, + * clicks, and scrolls, mapped to specific keys on the keyboard. + * The key codes here are the QWERTY key codes, and should be accessed via + * {@link MouseKeyEvent#getKeyCode(InputDevice)} + * so that it is mapped to the equivalent key on the keyboard layout of the keyboard device + * that is actually in use. + * </p> + */ public enum MouseKeyEvent { DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_7), UP_MOVE_OR_SCROLL(KeyEvent.KEYCODE_8), @@ -117,34 +145,64 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation RELEASE(KeyEvent.KEYCODE_COMMA), SCROLL_TOGGLE(KeyEvent.KEYCODE_PERIOD); - private final int mKeyCode; + private final int mLocationKeyCode; MouseKeyEvent(int enumValue) { - mKeyCode = enumValue; + mLocationKeyCode = enumValue; } - private static final SparseArray<MouseKeyEvent> VALUE_TO_ENUM_MAP = new SparseArray<>(); - - static { - for (MouseKeyEvent type : MouseKeyEvent.values()) { - VALUE_TO_ENUM_MAP.put(type.mKeyCode, type); - } + @VisibleForTesting + public final int getKeyCodeValue() { + return mLocationKeyCode; } - public final int getKeyCodeValue() { - return mKeyCode; + /** + * Get the key code associated with the given MouseKeyEvent for the given keyboard + * input device, taking into account its layout. + * The default is to return the keycode for the default layout (QWERTY). + * We check if the input device has been generated using {@link InputDevice#getGeneration()} + * to test with the default {@link MouseKeyEvent} values in the unit tests. + */ + public int getKeyCode(InputDevice inputDevice) { + if (inputDevice.getGeneration() == -1) { + return mLocationKeyCode; + } + return inputDevice.getKeyCodeForKeyLocation(mLocationKeyCode); } /** - * Convert int value of the key code to corresponding MouseEvent enum. If no matching - * value is found, this will return {@code null}. + * Convert int value of the key code to corresponding {@link MouseKeyEvent} + * enum for a particular device ID. + * If no matching value is found, this will return {@code null}. */ @Nullable - public static MouseKeyEvent from(int value) { - return VALUE_TO_ENUM_MAP.get(value); + public static MouseKeyEvent from(int keyCode, int deviceId, + SparseArray<SparseArray<MouseKeyEvent>> deviceKeyCodeMap) { + SparseArray<MouseKeyEvent> keyCodeToEnumMap = deviceKeyCodeMap.get(deviceId); + if (keyCodeToEnumMap != null) { + return keyCodeToEnumMap.get(keyCode); + } + return null; } } /** + * Create a map of key codes to their corresponding {@link MouseKeyEvent} values + * for a specific input device. + * The key for {@code mDeviceKeyCodeMap} is the deviceId. + * The key for {@code keyCodeToEnumMap} is the keycode for each + * {@link MouseKeyEvent} according to the keyboard layout of the input device. + */ + public void initializeDeviceToEnumMap(InputDevice inputDevice) { + int deviceId = inputDevice.getId(); + SparseArray<MouseKeyEvent> keyCodeToEnumMap = new SparseArray<>(); + for (MouseKeyEvent mouseKeyEventType : MouseKeyEvent.values()) { + int keyCode = mouseKeyEventType.getKeyCode(inputDevice); + keyCodeToEnumMap.put(keyCode, mouseKeyEventType); + } + mDeviceKeyCodeMap.put(deviceId, keyCodeToEnumMap); + } + + /** * Construct a new MouseKeysInterceptor. * * @param service The service to notify of key events @@ -152,8 +210,10 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation * @param displayId Display ID to send mouse events to */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) - public MouseKeysInterceptor(AccessibilityManagerService service, Looper looper, int displayId) { + public MouseKeysInterceptor(AccessibilityManagerService service, + InputManager inputManager, Looper looper, int displayId) { mAms = service; + mInputManager = inputManager; mHandler = new Handler(looper, this); // Create the virtual mouse on a separate thread since virtual device creation // should happen on an auxiliary thread, and not from the handler's thread. @@ -163,6 +223,9 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation mVirtualMouse = createVirtualMouse(displayId); }); mCreateVirtualMouseThread.start(); + // Register an input device listener to watch when input devices are + // added, removed or reconfigured. + mInputManager.registerInputDeviceListener(this, mHandler); } /** @@ -215,7 +278,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private void performMouseScrollAction(int keyCode) { - MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode); + MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from( + keyCode, mActiveInputDeviceId, mDeviceKeyCodeMap); float y = switch (mouseKeyEvent) { case UP_MOVE_OR_SCROLL -> 1.0f; case DOWN_MOVE_OR_SCROLL -> -1.0f; @@ -247,15 +311,18 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private void performMouseButtonAction(int keyCode) { - MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode); + MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from( + keyCode, mActiveInputDeviceId, mDeviceKeyCodeMap); int buttonCode = switch (mouseKeyEvent) { case LEFT_CLICK -> VirtualMouseButtonEvent.BUTTON_PRIMARY; case RIGHT_CLICK -> VirtualMouseButtonEvent.BUTTON_SECONDARY; default -> VirtualMouseButtonEvent.BUTTON_UNKNOWN; }; if (buttonCode != VirtualMouseButtonEvent.BUTTON_UNKNOWN) { - sendVirtualMouseButtonEvent(buttonCode, VirtualMouseButtonEvent.ACTION_BUTTON_PRESS); - sendVirtualMouseButtonEvent(buttonCode, VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE); + sendVirtualMouseButtonEvent(buttonCode, + VirtualMouseButtonEvent.ACTION_BUTTON_PRESS); + sendVirtualMouseButtonEvent(buttonCode, + VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE); } if (DEBUG) { if (buttonCode == VirtualMouseButtonEvent.BUTTON_UNKNOWN) { @@ -293,7 +360,9 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation private void performMousePointerAction(int keyCode) { float x = 0f; float y = 0f; - MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode); + MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from( + keyCode, mActiveInputDeviceId, mDeviceKeyCodeMap); + switch (mouseKeyEvent) { case DIAGONAL_DOWN_LEFT_MOVE -> { x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); @@ -339,18 +408,19 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation } } - private boolean isMouseKey(int keyCode) { - return MouseKeyEvent.VALUE_TO_ENUM_MAP.contains(keyCode); + private boolean isMouseKey(int keyCode, int deviceId) { + SparseArray<MouseKeyEvent> keyCodeToEnumMap = mDeviceKeyCodeMap.get(deviceId); + return keyCodeToEnumMap.contains(keyCode); } - private boolean isMouseButtonKey(int keyCode) { - return keyCode == MouseKeyEvent.LEFT_CLICK.getKeyCodeValue() - || keyCode == MouseKeyEvent.RIGHT_CLICK.getKeyCodeValue(); + private boolean isMouseButtonKey(int keyCode, InputDevice inputDevice) { + return keyCode == MouseKeyEvent.LEFT_CLICK.getKeyCode(inputDevice) + || keyCode == MouseKeyEvent.RIGHT_CLICK.getKeyCode(inputDevice); } - private boolean isMouseScrollKey(int keyCode) { - return keyCode == MouseKeyEvent.UP_MOVE_OR_SCROLL.getKeyCodeValue() - || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCodeValue(); + private boolean isMouseScrollKey(int keyCode, InputDevice inputDevice) { + return keyCode == MouseKeyEvent.UP_MOVE_OR_SCROLL.getKeyCode(inputDevice) + || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCode(inputDevice); } /** @@ -373,7 +443,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation } /** - * Handles key events and forwards mouse key events to the virtual mouse. + * Handles key events and forwards mouse key events to the virtual mouse on the handler thread. * * @param event The key event to handle. * @param policyFlags The policy flags associated with the key event. @@ -385,31 +455,45 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation mAms.getTraceManager().logTrace(LOG_TAG + ".onKeyEvent", FLAGS_INPUT_FILTER, "event=" + event + ";policyFlags=" + policyFlags); } + + mHandler.post(() -> { + onKeyEventInternal(event, policyFlags); + }); + } + + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + private void onKeyEventInternal(KeyEvent event, int policyFlags) { boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN; int keyCode = event.getKeyCode(); + mActiveInputDeviceId = event.getDeviceId(); + InputDevice inputDevice = mInputManager.getInputDevice(mActiveInputDeviceId); - if (!isMouseKey(keyCode)) { + if (!mDeviceKeyCodeMap.contains(mActiveInputDeviceId)) { + initializeDeviceToEnumMap(inputDevice); + } + + if (!isMouseKey(keyCode, mActiveInputDeviceId)) { // Pass non-mouse key events to the next handler super.onKeyEvent(event, policyFlags); } else if (isDown) { - if (keyCode == MouseKeyEvent.SCROLL_TOGGLE.getKeyCodeValue()) { + if (keyCode == MouseKeyEvent.SCROLL_TOGGLE.getKeyCode(inputDevice)) { mScrollToggleOn = !mScrollToggleOn; if (DEBUG) { Slog.d(LOG_TAG, "Scroll toggle " + (mScrollToggleOn ? "ON" : "OFF")); } - } else if (keyCode == MouseKeyEvent.HOLD.getKeyCodeValue()) { + } else if (keyCode == MouseKeyEvent.HOLD.getKeyCode(inputDevice)) { sendVirtualMouseButtonEvent( VirtualMouseButtonEvent.BUTTON_PRIMARY, VirtualMouseButtonEvent.ACTION_BUTTON_PRESS ); - } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCodeValue()) { + } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCode(inputDevice)) { sendVirtualMouseButtonEvent( VirtualMouseButtonEvent.BUTTON_PRIMARY, VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE ); - } else if (isMouseButtonKey(keyCode)) { + } else if (isMouseButtonKey(keyCode, inputDevice)) { performMouseButtonAction(keyCode); - } else if (mScrollToggleOn && isMouseScrollKey(keyCode)) { + } else if (mScrollToggleOn && isMouseScrollKey(keyCode, inputDevice)) { // If the scroll key is pressed down and no other key is active, // set it as the active key and send a message to scroll the pointer if (mActiveScrollKey == KEY_NOT_SET) { @@ -439,7 +523,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation mHandler.removeMessages(MESSAGE_SCROLL_MOUSE_POINTER); } else { Slog.i(LOG_TAG, "Dropping event with key code: '" + keyCode - + "', with no matching down event from deviceId = " + event.getDeviceId()); + + "', with no matching down event from deviceId = " + + event.getDeviceId()); } } } @@ -503,12 +588,40 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) @Override public void onDestroy() { - // Clear mouse state - mActiveMoveKey = KEY_NOT_SET; - mActiveScrollKey = KEY_NOT_SET; - mLastTimeKeyActionPerformed = 0; + mHandler.post(() -> { + // Clear mouse state + mActiveMoveKey = KEY_NOT_SET; + mActiveScrollKey = KEY_NOT_SET; + mLastTimeKeyActionPerformed = 0; + mDeviceKeyCodeMap.clear(); + }); mHandler.removeCallbacksAndMessages(null); mVirtualDevice.close(); } + + @Override + public void onInputDeviceAdded(int deviceId) { + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + mDeviceKeyCodeMap.remove(deviceId); + } + + /** + * The user can change the keyboard layout from settings at anytime, which would change + * key character map for that device. Hence, we should use this callback to + * update the key code to enum mapping if there is a change in the physical keyboard detected. + * + * @param deviceId The id of the input device that changed. + */ + @Override + public void onInputDeviceChanged(int deviceId) { + InputDevice inputDevice = mInputManager.getInputDevice(deviceId); + // Update the enum mapping only if input device that changed is a keyboard + if (inputDevice.isFullKeyboard() && !mDeviceKeyCodeMap.contains(deviceId)) { + initializeDeviceToEnumMap(inputDevice); + } + } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 99ad65d14ff2..0abd9bc3a433 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -354,7 +354,7 @@ public final class DisplayManagerService extends SystemService { new CopyOnWriteArrayList<>(); /** All {@link DisplayPowerController}s indexed by {@link LogicalDisplay} ID. */ - private final SparseArray<DisplayPowerControllerInterface> mDisplayPowerControllers = + private final SparseArray<DisplayPowerController> mDisplayPowerControllers = new SparseArray<>(); /** {@link DisplayBlanker} used by all {@link DisplayPowerController}s. */ @@ -726,7 +726,7 @@ public final class DisplayManagerService extends SystemService { if (logicalDisplay.getDisplayInfoLocked().type != Display.TYPE_INTERNAL) { return; } - final DisplayPowerControllerInterface dpc = mDisplayPowerControllers.get( + final DisplayPowerController dpc = mDisplayPowerControllers.get( logicalDisplay.getDisplayIdLocked()); if (dpc == null) { return; @@ -2058,7 +2058,7 @@ public final class DisplayManagerService extends SystemService { configurePreferredDisplayModeLocked(display); } - DisplayPowerControllerInterface dpc = addDisplayPowerControllerLocked(display); + DisplayPowerController dpc = addDisplayPowerControllerLocked(display); if (dpc != null) { final int leadDisplayId = display.getLeadDisplayIdLocked(); updateDisplayPowerControllerLeaderLocked(dpc, leadDisplayId); @@ -2067,7 +2067,7 @@ public final class DisplayManagerService extends SystemService { // that the follower display was added before the lead display. mLogicalDisplayMapper.forEachLocked(d -> { if (d.getLeadDisplayIdLocked() == displayId) { - DisplayPowerControllerInterface followerDpc = + DisplayPowerController followerDpc = mDisplayPowerControllers.get(d.getDisplayIdLocked()); if (followerDpc != null) { updateDisplayPowerControllerLeaderLocked(followerDpc, displayId); @@ -2151,7 +2151,7 @@ public final class DisplayManagerService extends SystemService { scheduleTraversalLocked(false); mPersistentDataStore.saveIfNeeded(); - DisplayPowerControllerInterface dpc = mDisplayPowerControllers.get(displayId); + DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { final int leadDisplayId = display.getLeadDisplayIdLocked(); updateDisplayPowerControllerLeaderLocked(dpc, leadDisplayId); @@ -2165,7 +2165,7 @@ public final class DisplayManagerService extends SystemService { } private void updateDisplayPowerControllerLeaderLocked( - @NonNull DisplayPowerControllerInterface dpc, int leadDisplayId) { + @NonNull DisplayPowerController dpc, int leadDisplayId) { if (dpc.getLeadDisplayId() == leadDisplayId) { // Lead display hasn't changed, nothing to do. return; @@ -2174,7 +2174,7 @@ public final class DisplayManagerService extends SystemService { // If it has changed, then we need to unregister from the previous leader if there was one. final int prevLeaderId = dpc.getLeadDisplayId(); if (prevLeaderId != Layout.NO_LEAD_DISPLAY) { - final DisplayPowerControllerInterface prevLeader = + final DisplayPowerController prevLeader = mDisplayPowerControllers.get(prevLeaderId); if (prevLeader != null) { prevLeader.removeDisplayBrightnessFollower(dpc); @@ -2183,7 +2183,7 @@ public final class DisplayManagerService extends SystemService { // And then, if it's following, register it with the new one. if (leadDisplayId != Layout.NO_LEAD_DISPLAY) { - final DisplayPowerControllerInterface newLeader = + final DisplayPowerController newLeader = mDisplayPowerControllers.get(leadDisplayId); if (newLeader != null) { newLeader.addDisplayBrightnessFollower(dpc); @@ -2224,7 +2224,7 @@ public final class DisplayManagerService extends SystemService { private void releaseDisplayAndEmitEvent(LogicalDisplay display, int event) { final int displayId = display.getDisplayIdLocked(); - final DisplayPowerControllerInterface dpc = + final DisplayPowerController dpc = mDisplayPowerControllers.removeReturnOld(displayId); if (dpc != null) { updateDisplayPowerControllerLeaderLocked(dpc, Layout.NO_LEAD_DISPLAY); @@ -2271,7 +2271,7 @@ public final class DisplayManagerService extends SystemService { private void handleLogicalDisplayDeviceStateTransitionLocked(@NonNull LogicalDisplay display) { final int displayId = display.getDisplayIdLocked(); - final DisplayPowerControllerInterface dpc = mDisplayPowerControllers.get(displayId); + final DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { final int leadDisplayId = display.getLeadDisplayIdLocked(); updateDisplayPowerControllerLeaderLocked(dpc, leadDisplayId); @@ -2692,14 +2692,14 @@ public final class DisplayManagerService extends SystemService { if (userId != mCurrentUserId) { return; } - DisplayPowerControllerInterface dpc = getDpcFromUniqueIdLocked(uniqueId); + DisplayPowerController dpc = getDpcFromUniqueIdLocked(uniqueId); if (dpc != null) { dpc.setBrightnessConfiguration(c, /* shouldResetShortTermModel= */ true); } } } - private DisplayPowerControllerInterface getDpcFromUniqueIdLocked(String uniqueId) { + private DisplayPowerController getDpcFromUniqueIdLocked(String uniqueId) { final DisplayDevice displayDevice = mDisplayDeviceRepo.getByUniqueIdLocked(uniqueId); final LogicalDisplay logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayDevice); if (logicalDisplay != null) { @@ -2740,7 +2740,7 @@ public final class DisplayManagerService extends SystemService { final BrightnessConfiguration config = getBrightnessConfigForDisplayWithPdsFallbackLocked(uniqueId, userSerial); if (config != null) { - final DisplayPowerControllerInterface dpc = mDisplayPowerControllers.get( + final DisplayPowerController dpc = mDisplayPowerControllers.get( logicalDisplay.getDisplayIdLocked()); if (dpc != null) { dpc.setBrightnessConfiguration(config, @@ -2987,7 +2987,7 @@ public final class DisplayManagerService extends SystemService { void setAutoBrightnessLoggingEnabled(boolean enabled) { synchronized (mSyncRoot) { - final DisplayPowerControllerInterface displayPowerController = + final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(Display.DEFAULT_DISPLAY); if (displayPowerController != null) { displayPowerController.setAutoBrightnessLoggingEnabled(enabled); @@ -2997,7 +2997,7 @@ public final class DisplayManagerService extends SystemService { void setDisplayWhiteBalanceLoggingEnabled(boolean enabled) { synchronized (mSyncRoot) { - final DisplayPowerControllerInterface displayPowerController = + final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(Display.DEFAULT_DISPLAY); if (displayPowerController != null) { displayPowerController.setDisplayWhiteBalanceLoggingEnabled(enabled); @@ -3023,7 +3023,7 @@ public final class DisplayManagerService extends SystemService { void setAmbientColorTemperatureOverride(float cct) { synchronized (mSyncRoot) { - final DisplayPowerControllerInterface displayPowerController = + final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(Display.DEFAULT_DISPLAY); if (displayPowerController != null) { displayPowerController.setAmbientColorTemperatureOverride(cct); @@ -3033,7 +3033,7 @@ public final class DisplayManagerService extends SystemService { void setDockedAndIdleEnabled(boolean enabled, int displayId) { synchronized (mSyncRoot) { - final DisplayPowerControllerInterface displayPowerController = + final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(displayId); if (displayPowerController != null) { displayPowerController.setAutomaticScreenBrightnessMode(enabled @@ -3571,7 +3571,7 @@ public final class DisplayManagerService extends SystemService { } @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) - private DisplayPowerControllerInterface addDisplayPowerControllerLocked( + private DisplayPowerController addDisplayPowerControllerLocked( LogicalDisplay display) { if (mPowerHandler == null) { // initPowerManagement has not yet been called. @@ -3585,7 +3585,7 @@ public final class DisplayManagerService extends SystemService { final int userSerial = getUserManager().getUserSerialNumber(mContext.getUserId()); final BrightnessSetting brightnessSetting = new BrightnessSetting(userSerial, mPersistentDataStore, display, mSyncRoot); - final DisplayPowerControllerInterface displayPowerController; + final DisplayPowerController displayPowerController; // If display is internal and has a HighBrightnessModeMetadata mapping, use that. // Or create a new one and use that. @@ -4373,7 +4373,7 @@ public final class DisplayManagerService extends SystemService { uniqueId, userSerial); if (config == null) { // Get default configuration - DisplayPowerControllerInterface dpc = getDpcFromUniqueIdLocked(uniqueId); + DisplayPowerController dpc = getDpcFromUniqueIdLocked(uniqueId); if (dpc != null) { config = dpc.getDefaultBrightnessConfiguration(); } @@ -4427,7 +4427,7 @@ public final class DisplayManagerService extends SystemService { if (display == null || !display.isEnabledLocked()) { return null; } - DisplayPowerControllerInterface dpc = mDisplayPowerControllers.get(displayId); + DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { return dpc.getBrightnessInfo(); } @@ -4472,7 +4472,7 @@ public final class DisplayManagerService extends SystemService { final long token = Binder.clearCallingIdentity(); try { synchronized (mSyncRoot) { - DisplayPowerControllerInterface dpc = mDisplayPowerControllers.get(displayId); + DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { dpc.setBrightness(brightness); } @@ -4492,7 +4492,7 @@ public final class DisplayManagerService extends SystemService { final long token = Binder.clearCallingIdentity(); try { synchronized (mSyncRoot) { - DisplayPowerControllerInterface dpc = mDisplayPowerControllers.get(displayId); + DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { brightness = dpc.getScreenBrightnessSetting(); } @@ -4819,7 +4819,7 @@ public final class DisplayManagerService extends SystemService { id).getPrimaryDisplayDeviceLocked(); final int flags = displayDevice.getDisplayDeviceInfoLocked().flags; if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) { - final DisplayPowerControllerInterface displayPowerController = + final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(id); if (displayPowerController != null) { ready &= displayPowerController.requestPowerState(request, @@ -5200,7 +5200,7 @@ public final class DisplayManagerService extends SystemService { return null; } - DisplayPowerControllerInterface displayPowerController = + DisplayPowerController displayPowerController = mDisplayPowerControllers.get(logicalDisplay.getDisplayIdLocked()); if (displayPowerController == null) { Slog.w(TAG, diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java index a188e79e4c8b..b05a96ea0439 100644 --- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java +++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java @@ -39,12 +39,12 @@ public class DisplayOffloadSessionImpl implements DisplayManagerInternal.Display @Nullable private final DisplayManagerInternal.DisplayOffloader mDisplayOffloader; - private final DisplayPowerControllerInterface mDisplayPowerController; + private final DisplayPowerController mDisplayPowerController; private boolean mIsActive; public DisplayOffloadSessionImpl( @Nullable DisplayManagerInternal.DisplayOffloader displayOffloader, - DisplayPowerControllerInterface displayPowerController) { + DisplayPowerController displayPowerController) { mDisplayOffloader = displayOffloader; mDisplayPowerController = displayPowerController; } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index bf559c10b0ba..bb2bed7281f7 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -126,7 +126,7 @@ import java.util.Objects; * slower by changing the "animator duration scale" option in Development Settings. */ final class DisplayPowerController implements AutomaticBrightnessController.Callbacks, - DisplayWhiteBalanceController.Callbacks, DisplayPowerControllerInterface { + DisplayWhiteBalanceController.Callbacks{ private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked"; private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked"; @@ -481,7 +481,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // DPCs following the brightness of this DPC. This is used in concurrent displays mode - there // is one lead display, the additional displays follow the brightness value of the lead display. @GuardedBy("mLock") - private final SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers = + private final SparseArray<DisplayPowerController> mDisplayBrightnessFollowers = new SparseArray(); private boolean mBootCompleted; @@ -679,7 +679,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call /** * Returns true if the proximity sensor screen-off function is available. */ - @Override public boolean isProximitySensorAvailable() { return mDisplayPowerProximityStateController.isProximitySensorAvailable(); } @@ -691,7 +690,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call * @param includePackage if false will null out the package name in events */ @Nullable - @Override public ParceledListSlice<BrightnessChangeEvent> getBrightnessEvents( @UserIdInt int userId, boolean includePackage) { if (mBrightnessTracker == null) { @@ -700,7 +698,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call return mBrightnessTracker.getEvents(userId, includePackage); } - @Override public void onSwitchUser(@UserIdInt int newUserId, int userSerial, float newBrightness) { Message msg = mHandler.obtainMessage(MSG_SWITCH_USER, newUserId, userSerial, newBrightness); mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()); @@ -737,7 +734,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } @Nullable - @Override public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats( @UserIdInt int userId) { if (mBrightnessTracker == null) { @@ -749,7 +745,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call /** * Persist the brightness slider events and ambient brightness stats to disk. */ - @Override public void persistBrightnessTrackerState() { if (mBrightnessTracker != null) { mBrightnessTracker.persistBrightnessTrackerState(); @@ -806,7 +801,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } - @Override public void overrideDozeScreenState(int displayState, @Display.StateReason int reason) { Slog.i(TAG, "New offload doze override: " + Display.stateToString(displayState)); if (mDisplayOffloadSession != null @@ -833,7 +827,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } - @Override public void setDisplayOffloadSession(DisplayOffloadSession session) { if (session == mDisplayOffloadSession) { return; @@ -842,7 +835,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mDisplayOffloadSession = session; } - @Override public BrightnessConfiguration getDefaultBrightnessConfiguration() { if (mAutomaticBrightnessController == null) { return null; @@ -857,7 +849,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call * * Make sure DisplayManagerService.mSyncRoot lock is held when this is called */ - @Override public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata, int leadDisplayId) { mLeadDisplayId = leadDisplayId; final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); @@ -939,7 +930,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call * This method should be called when the DisplayPowerController is no longer in use; i.e. when * the {@link #mDisplayId display} has been removed. */ - @Override public void stop() { synchronized (mLock) { clearDisplayBrightnessFollowersLocked(); @@ -1216,7 +1206,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } - @Override public void setAutomaticScreenBrightnessMode( @AutomaticBrightnessController.AutomaticBrightnessMode int mode) { Message msg = mHandler.obtainMessage(); @@ -1314,7 +1303,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call boolean mustInitialize = false; mBrightnessReasonTemp.set(null); mTempBrightnessEvent.reset(); - SparseArray<DisplayPowerControllerInterface> displayBrightnessFollowers; + SparseArray<DisplayPowerController> displayBrightnessFollowers; synchronized (mLock) { if (mStopped) { return; @@ -1547,7 +1536,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call float ambientLux = mAutomaticBrightnessController == null ? 0 : mAutomaticBrightnessController.getAmbientLux(); for (int i = 0; i < displayBrightnessFollowers.size(); i++) { - DisplayPowerControllerInterface follower = displayBrightnessFollowers.valueAt(i); + DisplayPowerController follower = displayBrightnessFollowers.valueAt(i); follower.setBrightnessToFollow(rawBrightnessState, mDisplayBrightnessController.convertToNits(rawBrightnessState), ambientLux, slowChange); @@ -1904,7 +1893,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } - @Override public void updateBrightness() { sendUpdatePowerState(); } @@ -1913,12 +1901,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call * Ignores the proximity sensor until the sensor state changes, but only if the sensor is * currently enabled and forcing the screen to be dark. */ - @Override public void ignoreProximitySensorUntilChanged() { mDisplayPowerProximityStateController.ignoreProximitySensorUntilChanged(); } - @Override public void setBrightnessConfiguration(BrightnessConfiguration c, boolean shouldResetShortTermModel) { Message msg = mHandler.obtainMessage(MSG_CONFIGURE_BRIGHTNESS, @@ -1926,28 +1912,24 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call msg.sendToTarget(); } - @Override public void setTemporaryBrightness(float brightness) { Message msg = mHandler.obtainMessage(MSG_SET_TEMPORARY_BRIGHTNESS, Float.floatToIntBits(brightness), 0 /*unused*/); msg.sendToTarget(); } - @Override public void setTemporaryAutoBrightnessAdjustment(float adjustment) { Message msg = mHandler.obtainMessage(MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT, Float.floatToIntBits(adjustment), 0 /*unused*/); msg.sendToTarget(); } - @Override public void setBrightnessFromOffload(float brightness) { Message msg = mHandler.obtainMessage(MSG_SET_BRIGHTNESS_FROM_OFFLOAD, Float.floatToIntBits(brightness), 0 /*unused*/); mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()); } - @Override public float[] getAutoBrightnessLevels( @AutomaticBrightnessController.AutomaticBrightnessMode int mode) { int preset = Settings.System.getIntForUser(mContext.getContentResolver(), @@ -1956,7 +1938,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(mode, preset); } - @Override public float[] getAutoBrightnessLuxLevels( @AutomaticBrightnessController.AutomaticBrightnessMode int mode) { int preset = Settings.System.getIntForUser(mContext.getContentResolver(), @@ -1965,7 +1946,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(mode, preset); } - @Override public BrightnessInfo getBrightnessInfo() { synchronized (mCachedBrightnessInfo) { return new BrightnessInfo( @@ -1979,7 +1959,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } - @Override public void onBootCompleted() { Message msg = mHandler.obtainMessage(MSG_BOOT_COMPLETED); mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()); @@ -2495,18 +2474,14 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } - - @Override public float getScreenBrightnessSetting() { return mDisplayBrightnessController.getScreenBrightnessSetting(); } - @Override public float getDozeBrightnessForOffload() { return mDisplayBrightnessController.getCurrentBrightness() * mDozeScaleFactor; } - @Override public void setBrightness(float brightness) { // After HBMController and NBMController migration to Clampers framework // currentBrightnessMax should be taken from clampers controller @@ -2515,7 +2490,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessRangeController.getCurrentBrightnessMax()); } - @Override public void setBrightness(float brightness, int userSerial) { // After HBMController and NBMController migration to Clampers framework // currentBrightnessMax should be taken from clampers controller @@ -2524,17 +2498,14 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessRangeController.getCurrentBrightnessMax()); } - @Override public int getDisplayId() { return mDisplayId; } - @Override public int getLeadDisplayId() { return mLeadDisplayId; } - @Override public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux, boolean slowChange) { mBrightnessRangeController.onAmbientLuxChange(ambientLux); @@ -2595,16 +2566,14 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mAutomaticBrightnessController.getLastSensorTimestamps()); } - @Override - public void addDisplayBrightnessFollower(DisplayPowerControllerInterface follower) { + public void addDisplayBrightnessFollower(DisplayPowerController follower) { synchronized (mLock) { mDisplayBrightnessFollowers.append(follower.getDisplayId(), follower); sendUpdatePowerStateLocked(); } } - @Override - public void removeDisplayBrightnessFollower(DisplayPowerControllerInterface follower) { + public void removeDisplayBrightnessFollower(DisplayPowerController follower) { synchronized (mLock) { mDisplayBrightnessFollowers.remove(follower.getDisplayId()); mHandler.postAtTime(() -> follower.setBrightnessToFollow( @@ -2616,7 +2585,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call @GuardedBy("mLock") private void clearDisplayBrightnessFollowersLocked() { for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) { - DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i); + DisplayPowerController follower = mDisplayBrightnessFollowers.valueAt(i); mHandler.postAtTime(() -> follower.setBrightnessToFollow( PowerManager.BRIGHTNESS_INVALID_FLOAT, BrightnessMappingStrategy.INVALID_NITS, /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis()); @@ -2624,7 +2593,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mDisplayBrightnessFollowers.clear(); } - @Override public void dump(final PrintWriter pw) { synchronized (mLock) { pw.println(); @@ -3161,19 +3129,17 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } - @Override public void setAutoBrightnessLoggingEnabled(boolean enabled) { if (mAutomaticBrightnessController != null) { mAutomaticBrightnessController.setLoggingEnabled(enabled); } } - @Override // DisplayWhiteBalanceController.Callbacks + // DisplayWhiteBalanceController.Callbacks public void updateWhiteBalance() { sendUpdatePowerState(); } - @Override public void setDisplayWhiteBalanceLoggingEnabled(boolean enabled) { Message msg = mHandler.obtainMessage(); msg.what = MSG_SET_DWBC_LOGGING_ENABLED; @@ -3181,7 +3147,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call msg.sendToTarget(); } - @Override public void setAmbientColorTemperatureOverride(float cct) { Message msg = mHandler.obtainMessage(); msg.what = MSG_SET_DWBC_COLOR_OVERRIDE; diff --git a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java deleted file mode 100644 index d28578ad571f..000000000000 --- a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.display; - -import android.content.pm.ParceledListSlice; -import android.hardware.display.AmbientBrightnessDayStats; -import android.hardware.display.BrightnessChangeEvent; -import android.hardware.display.BrightnessConfiguration; -import android.hardware.display.BrightnessInfo; -import android.hardware.display.DisplayManagerInternal; -import android.os.PowerManager; -import android.view.Display; - -import java.io.PrintWriter; - -/** - * An interface to manage the display's power state and brightness - */ -public interface DisplayPowerControllerInterface { - /** - * Notified when the display is changed. - * - * We use this to apply any changes that might be needed when displays get swapped on foldable - * devices, when layouts change, etc. - * - * Must be called while holding the SyncRoot lock. - * - * @param hbmInfo The high brightness mode metadata, like - * remaining time and hbm events, for the corresponding - * physical display, to make sure we stay within the safety margins. - * @param leadDisplayId The display who is considered our "leader" for things like brightness. - */ - void onDisplayChanged(HighBrightnessModeMetadata hbmInfo, int leadDisplayId); - - /** - * Unregisters all listeners and interrupts all running threads; halting future work. - * - * This method should be called when the DisplayPowerController is no longer in use; i.e. when - * the display has been removed. - */ - void stop(); - - /** - * Used to update the display's BrightnessConfiguration - * @param config The new BrightnessConfiguration - */ - void setBrightnessConfiguration(BrightnessConfiguration config, - boolean shouldResetShortTermModel); - - /** - * Used to set the ambient color temperature of the Display - * @param ambientColorTemperature The target ambientColorTemperature - */ - void setAmbientColorTemperatureOverride(float ambientColorTemperature); - - /** - * Used to decide the associated AutomaticBrightnessController's BrightnessMode - * @param mode The auto-brightness mode - */ - void setAutomaticScreenBrightnessMode( - @AutomaticBrightnessController.AutomaticBrightnessMode int mode); - - /** - * Used to enable/disable the logging of the WhileBalance associated entities - * @param enabled Flag which represents if the logging is the be enabled - */ - void setDisplayWhiteBalanceLoggingEnabled(boolean enabled); - - /** - * Used to dump the state. - * @param writer The PrintWriter used to dump the state. - */ - void dump(PrintWriter writer); - - /** - * Used to get the ambient brightness stats - */ - ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(int userId); - - /** - * Get the default brightness configuration - */ - BrightnessConfiguration getDefaultBrightnessConfiguration(); - - /** - * Set the screen brightness of the associated display - * @param brightness The value to which the brightness is to be set - */ - void setBrightness(float brightness); - - /** - * Set the screen brightness of the associated display - * @param brightness The value to which the brightness is to be set - * @param userSerial The user for which the brightness value is to be set. - */ - void setBrightness(float brightness, int userSerial); - - /** - * Checks if the proximity sensor is available - */ - boolean isProximitySensorAvailable(); - - /** - * Persist the brightness slider events and ambient brightness stats to disk. - */ - void persistBrightnessTrackerState(); - - /** - * Ignores the proximity sensor until the sensor state changes, but only if the sensor is - * currently enabled and forcing the screen to be dark. - */ - void ignoreProximitySensorUntilChanged(); - - /** - * Requests a new power state. - * - * @param request The requested power state. - * @param waitForNegativeProximity If true, issues a request to wait for - * negative proximity before turning the screen back on, - * assuming the screen was turned off by the proximity sensor. - * @return True if display is ready, false if there are important changes that must - * be made asynchronously. - */ - boolean requestPowerState(DisplayManagerInternal.DisplayPowerRequest request, - boolean waitForNegativeProximity); - - /** - * Overrides the current doze screen state. - * - * @param displayState the new doze display state. - * @param reason the reason behind the new doze display state. - */ - void overrideDozeScreenState(int displayState, @Display.StateReason int reason); - - void setDisplayOffloadSession(DisplayManagerInternal.DisplayOffloadSession session); - - /** - * Sets up the temporary autobrightness adjustment when the user is yet to settle down to a - * value. - */ - void setTemporaryAutoBrightnessAdjustment(float adjustment); - - /** - * Sets temporary brightness from the offload chip until we get a brightness value from - * the light sensor. - * @param brightness The brightness value between {@link PowerManager.BRIGHTNESS_MIN} and - * {@link PowerManager.BRIGHTNESS_MAX}. Values outside of that range will be ignored. - */ - void setBrightnessFromOffload(float brightness); - - /** - * Gets the screen brightness setting - */ - float getScreenBrightnessSetting(); - - /** - * Gets the brightness value used when the device is in doze - */ - float getDozeBrightnessForOffload(); - - /** - * Sets up the temporary brightness for the associated display - */ - void setTemporaryBrightness(float brightness); - - /** - * Gets the associated {@link BrightnessInfo} - */ - BrightnessInfo getBrightnessInfo(); - - /** - * Get the {@link BrightnessChangeEvent}s for the specified user. - */ - ParceledListSlice<BrightnessChangeEvent> getBrightnessEvents(int userId, boolean hasUsageStats); - - /** - * Sets up the logging for the associated {@link AutomaticBrightnessController} - * @param enabled Flag to represent if the logging is to be enabled - */ - void setAutoBrightnessLoggingEnabled(boolean enabled); - - /** - * Handles the changes to be done to update the brightness when the user is changed - * @param newUserId The new userId - * @param userSerial The serial number of the new user - * @param newBrightness The brightness for the new user - */ - void onSwitchUser(int newUserId, int userSerial, float newBrightness); - - /** - * Get the ID of the display associated with this DPC. - * @return The display ID - */ - int getDisplayId(); - - /** - * Get the ID of the display that is the leader of this DPC. - * - * Note that this is different than the display associated with the DPC. The leader is another - * display which we follow for things like brightness. - * - * Must be called while holding the SyncRoot lock. - */ - int getLeadDisplayId(); - - /** - * Set the brightness to follow if this is an additional display in a set of concurrent - * displays. - * @param leadDisplayBrightness The brightness of the lead display in the set of concurrent - * displays - * @param nits The brightness value in nits if the device supports nits. Set to a negative - * number otherwise. - * @param ambientLux The lux value that will be passed to {@link HighBrightnessModeController} - * @param slowChange Indicates whether we should slowly animate to the given brightness value. - */ - void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux, - boolean slowChange); - - /** - * Add an additional display that will copy the brightness value from this display. This is used - * when the device is in concurrent displays mode. - * @param follower The DPC that should copy the brightness value from this DPC - */ - void addDisplayBrightnessFollower(DisplayPowerControllerInterface follower); - - /** - * Removes the given display from the list of brightness followers. - * @param follower The DPC to remove from the followers list - */ - void removeDisplayBrightnessFollower(DisplayPowerControllerInterface follower); - - /** - * Indicate that boot has been completed and the screen is ready to update. - */ - void onBootCompleted(); - - /** - * Get the brightness levels used to determine automatic brightness based on lux levels. - * @param mode The auto-brightness mode - * @return The brightness levels for the specified mode. The values are between - * {@link PowerManager.BRIGHTNESS_MIN} and {@link PowerManager.BRIGHTNESS_MAX}. - */ - float[] getAutoBrightnessLevels( - @AutomaticBrightnessController.AutomaticBrightnessMode int mode); - - /** - * Get the lux levels used to determine automatic brightness. - * @param mode The auto-brightness mode - * @return The lux levels for the specified mode - */ - float[] getAutoBrightnessLuxLevels( - @AutomaticBrightnessController.AutomaticBrightnessMode int mode); -} diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java index 5fca771c48b9..7785ffb4b17a 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java +++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java @@ -16,23 +16,69 @@ package com.android.server.input.debug; +import android.annotation.NonNull; import android.content.Context; +import android.content.res.Configuration; import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.util.Slog; import android.view.Gravity; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; -public class TouchpadDebugView extends LinearLayout { +import java.util.Objects; +public class TouchpadDebugView extends LinearLayout { /** * Input device ID for the touchpad that this debug view is displaying. */ private final int mTouchpadId; + @NonNull + private final WindowManager mWindowManager; + + @NonNull + private final WindowManager.LayoutParams mWindowLayoutParams; + + private final int mTouchSlop; + + private float mTouchDownX; + private float mTouchDownY; + private int mScreenWidth; + private int mScreenHeight; + private int mWindowLocationBeforeDragX; + private int mWindowLocationBeforeDragY; + public TouchpadDebugView(Context context, int touchpadId) { super(context); mTouchpadId = touchpadId; + mWindowManager = + Objects.requireNonNull(getContext().getSystemService(WindowManager.class)); init(context); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + // TODO(b/360137366): Use the hardware properties to initialise layout parameters. + mWindowLayoutParams = new WindowManager.LayoutParams(); + mWindowLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + mWindowLayoutParams.privateFlags |= + WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + mWindowLayoutParams.setFitInsetsTypes(0); + mWindowLayoutParams.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; + mWindowLayoutParams.setTitle("TouchpadDebugView - display " + mContext.getDisplayId()); + + mWindowLayoutParams.x = 40; + mWindowLayoutParams.y = 100; + mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; + mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; } private void init(Context context) { @@ -43,14 +89,12 @@ public class TouchpadDebugView extends LinearLayout { setBackgroundColor(Color.TRANSPARENT); // TODO(b/286551975): Replace this content with the touchpad debug view. - TextView textView1 = new TextView(context); textView1.setBackgroundColor(Color.parseColor("#FFFF0000")); textView1.setTextSize(20); textView1.setText("Touchpad Debug View 1"); textView1.setGravity(Gravity.CENTER); textView1.setTextColor(Color.WHITE); - textView1.setLayoutParams(new LayoutParams(1000, 200)); TextView textView2 = new TextView(context); @@ -63,9 +107,98 @@ public class TouchpadDebugView extends LinearLayout { addView(textView1); addView(textView2); + + updateScreenDimensions(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float deltaX; + float deltaY; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mWindowLocationBeforeDragX = mWindowLayoutParams.x; + mWindowLocationBeforeDragY = mWindowLayoutParams.y; + mTouchDownX = event.getRawX() - mWindowLocationBeforeDragX; + mTouchDownY = event.getRawY() - mWindowLocationBeforeDragY; + return true; + + case MotionEvent.ACTION_MOVE: + deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX; + deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY; + Slog.d("TouchpadDebugView", "Slop = " + mTouchSlop); + if (isSlopExceeded(deltaX, deltaY)) { + Slog.d("TouchpadDebugView", "Slop exceeded"); + mWindowLayoutParams.x = + Math.max(0, Math.min((int) (event.getRawX() - mTouchDownX), + mScreenWidth - this.getWidth())); + mWindowLayoutParams.y = + Math.max(0, Math.min((int) (event.getRawY() - mTouchDownY), + mScreenHeight - this.getHeight())); + + Slog.d("TouchpadDebugView", "New position X: " + + mWindowLayoutParams.x + ", Y: " + mWindowLayoutParams.y); + + mWindowManager.updateViewLayout(this, mWindowLayoutParams); + } + return true; + + case MotionEvent.ACTION_UP: + deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX; + deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY; + if (!isSlopExceeded(deltaX, deltaY)) { + performClick(); + } + return true; + + case MotionEvent.ACTION_CANCEL: + // Move the window back to the original position + mWindowLayoutParams.x = mWindowLocationBeforeDragX; + mWindowLayoutParams.y = mWindowLocationBeforeDragY; + mWindowManager.updateViewLayout(this, mWindowLayoutParams); + return true; + + default: + return super.onTouchEvent(event); + } + } + + @Override + public boolean performClick() { + super.performClick(); + Slog.d("TouchpadDebugView", "You clicked me!"); + return true; + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + updateScreenDimensions(); + + // Adjust view position to stay within screen bounds after rotation + mWindowLayoutParams.x = + Math.max(0, Math.min(mWindowLayoutParams.x, mScreenWidth - getWidth())); + mWindowLayoutParams.y = + Math.max(0, Math.min(mWindowLayoutParams.y, mScreenHeight - getHeight())); + mWindowManager.updateViewLayout(this, mWindowLayoutParams); + } + + private boolean isSlopExceeded(float deltaX, float deltaY) { + return deltaX * deltaX + deltaY * deltaY >= mTouchSlop * mTouchSlop; + } + + private void updateScreenDimensions() { + Rect windowBounds = + mWindowManager.getCurrentWindowMetrics().getBounds(); + mScreenWidth = windowBounds.width(); + mScreenHeight = windowBounds.height(); } public int getTouchpadId() { return mTouchpadId; } + + public WindowManager.LayoutParams getWindowLayoutParams() { + return mWindowLayoutParams; + } } diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java index c7760c63fec5..1e59167fdb9f 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java +++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java @@ -18,14 +18,12 @@ package com.android.server.input.debug; import android.annotation.Nullable; import android.content.Context; -import android.graphics.PixelFormat; import android.hardware.display.DisplayManager; import android.hardware.input.InputManager; import android.os.Handler; import android.os.Looper; import android.util.Slog; import android.view.Display; -import android.view.Gravity; import android.view.InputDevice; import android.view.WindowManager; @@ -36,12 +34,14 @@ import java.util.Objects; public class TouchpadDebugViewController { - private static final String TAG = "TouchpadDebugViewController"; + private static final String TAG = "TouchpadDebugView"; private final Context mContext; private final Handler mHandler; + @Nullable private TouchpadDebugView mTouchpadDebugView; + private final InputManagerService mInputManagerService; public TouchpadDebugViewController(Context context, Looper looper, @@ -95,32 +95,15 @@ public class TouchpadDebugViewController { mContext.getSystemService(WindowManager.class)); mTouchpadDebugView = new TouchpadDebugView(mContext, touchpadId); + final WindowManager.LayoutParams mWindowLayoutParams = + mTouchpadDebugView.getWindowLayoutParams(); - final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); - lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; - lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - lp.setFitInsetsTypes(0); - lp.layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - lp.format = PixelFormat.TRANSLUCENT; - lp.setTitle("TouchpadDebugView - display " + mContext.getDisplayId()); - lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; - - lp.x = 40; - lp.y = 100; - lp.width = WindowManager.LayoutParams.WRAP_CONTENT; - lp.height = WindowManager.LayoutParams.WRAP_CONTENT; - lp.gravity = Gravity.TOP | Gravity.LEFT; - - wm.addView(mTouchpadDebugView, lp); + wm.addView(mTouchpadDebugView, mWindowLayoutParams); Slog.d(TAG, "Touchpad debug view created."); TouchpadHardwareProperties mTouchpadHardwareProperties = mInputManagerService.getTouchpadHardwareProperties( touchpadId); - // TODO(b/360137366): Use the hardware properties to initialise layout parameters. if (mTouchpadHardwareProperties != null) { Slog.d(TAG, mTouchpadHardwareProperties.toString()); } else { diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index f96706e474be..749025b0cf40 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -16,6 +16,7 @@ package com.android.server.policy; +import static android.Manifest.permission.CREATE_VIRTUAL_DEVICE; import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; import static android.Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW; import static android.Manifest.permission.SYSTEM_ALERT_WINDOW; @@ -59,13 +60,18 @@ import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG; +import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION; import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION; import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG; +import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; +import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION; +import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType; import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD; @@ -3058,7 +3064,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { /** {@inheritDoc} */ @Override public int checkAddPermission(int type, boolean isRoundedCornerOverlay, String packageName, - int[] outAppOp) { + int[] outAppOp, int displayId) { if (isRoundedCornerOverlay && mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW) != PERMISSION_GRANTED) { return ADD_PERMISSION_DENIED; @@ -3098,6 +3104,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { case TYPE_VOICE_INTERACTION: case TYPE_QS_DIALOG: case TYPE_NAVIGATION_BAR_PANEL: + case TYPE_STATUS_BAR: + case TYPE_NOTIFICATION_SHADE: + case TYPE_NAVIGATION_BAR: + case TYPE_STATUS_BAR_ADDITIONAL: + case TYPE_STATUS_BAR_SUB_PANEL: + case TYPE_VOICE_INTERACTION_STARTING: // The window manager will check these. return ADD_OKAY; } @@ -3141,6 +3153,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { return ADD_OKAY; } + // Allow virtual device owners to add overlays on the displays they own. + if (mWindowManagerFuncs.isCallerVirtualDeviceOwner(displayId, callingUid) + && mContext.checkCallingOrSelfPermission(CREATE_VIRTUAL_DEVICE) + == PERMISSION_GRANTED) { + return ADD_OKAY; + } + // check if user has enabled this operation. SecurityException will be thrown if this app // has not been allowed by the user. The reason to use "noteOp" (instead of checkOp) is to // make sure the usage is logged. @@ -3581,18 +3600,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (down) { int direction = keyCode == KeyEvent.KEYCODE_BRIGHTNESS_UP ? 1 : -1; - // Disable autobrightness if it's on - int auto = Settings.System.getIntForUser( - mContext.getContentResolver(), - Settings.System.SCREEN_BRIGHTNESS_MODE, - Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, - UserHandle.USER_CURRENT_OR_SELF); - if (auto != 0) { - Settings.System.putIntForUser(mContext.getContentResolver(), - Settings.System.SCREEN_BRIGHTNESS_MODE, - Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, - UserHandle.USER_CURRENT_OR_SELF); - } int screenDisplayId = displayId < 0 ? DEFAULT_DISPLAY : displayId; float minLinearBrightness = mPowerManager.getBrightnessConstraint( diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java index 989c8a802b36..892af6bec534 100644 --- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java +++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java @@ -362,6 +362,12 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { * Invoked when a screenshot is taken of the given display to notify registered listeners. */ List<ComponentName> notifyScreenshotListeners(int displayId); + + /** + * Returns whether the given UID is the owner of a virtual device, which the given display + * belongs to. + */ + boolean isCallerVirtualDeviceOwner(int displayId, int callingUid); } /** @@ -421,6 +427,7 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { * @param packageName package name * @param outAppOp First element will be filled with the app op corresponding to * this window, or OP_NONE. + * @param displayId The display on which this window is being added. * * @return {@link WindowManagerGlobal#ADD_OKAY} if the add can proceed; * else an error code, usually @@ -429,7 +436,7 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { * @see WindowManager.LayoutParams#PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY */ int checkAddPermission(int type, boolean isRoundedCornerOverlay, String packageName, - int[] outAppOp); + int[] outAppOp, int displayId); /** * After the window manager has computed the current configuration based diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java index 822ec2eb79b0..6847a5c699ac 100644 --- a/services/core/java/com/android/server/power/ThermalManagerService.java +++ b/services/core/java/com/android/server/power/ThermalManagerService.java @@ -54,6 +54,7 @@ import android.os.ShellCallback; import android.os.ShellCommand; import android.os.SystemClock; import android.os.Temperature; +import android.os.Trace; import android.util.ArrayMap; import android.util.EventLog; import android.util.Slog; @@ -247,6 +248,7 @@ public class ThermalManagerService extends SystemService { private void setStatusLocked(int newStatus) { if (newStatus != mStatus) { + Trace.traceCounter(Trace.TRACE_TAG_POWER, "ThermalManagerService.status", newStatus); mStatus = newStatus; notifyStatusListenersLocked(); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index e90a2c90bfab..9a3ad2d85de6 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -107,7 +107,6 @@ import android.app.servertransaction.LaunchActivityItem; import android.app.servertransaction.PauseActivityItem; import android.app.servertransaction.ResumeActivityItem; import android.app.servertransaction.StopActivityItem; -import android.companion.virtual.VirtualDeviceManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -158,6 +157,7 @@ import com.android.server.LocalServices; import com.android.server.am.ActivityManagerService; import com.android.server.am.HostingRecord; import com.android.server.am.UserState; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.pm.SaferIntentUtils; import com.android.server.utils.Slogf; import com.android.server.wm.ActivityMetricsLogger.LaunchingState; @@ -285,7 +285,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { private WindowManagerService mWindowManager; private AppOpsManager mAppOpsManager; - private VirtualDeviceManager mVirtualDeviceManager; + private VirtualDeviceManagerInternal mVirtualDeviceManagerInternal; /** Common synchronization logic used to save things to disks. */ PersisterQueue mPersisterQueue; @@ -1298,16 +1298,24 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { if (displayId == DEFAULT_DISPLAY || displayId == INVALID_DISPLAY) { return Context.DEVICE_ID_DEFAULT; } - if (mVirtualDeviceManager == null) { + if (mVirtualDeviceManagerInternal == null) { if (mService.mHasCompanionDeviceSetupFeature) { - mVirtualDeviceManager = - mService.mContext.getSystemService(VirtualDeviceManager.class); + mVirtualDeviceManagerInternal = + LocalServices.getService(VirtualDeviceManagerInternal.class); } - if (mVirtualDeviceManager == null) { + if (mVirtualDeviceManagerInternal == null) { return Context.DEVICE_ID_DEFAULT; } } - return mVirtualDeviceManager.getDeviceIdForDisplayId(displayId); + return mVirtualDeviceManagerInternal.getDeviceIdForDisplayId(displayId); + } + + boolean isDeviceOwnerUid(int displayId, int callingUid) { + final int deviceId = getDeviceIdForDisplayId(displayId); + if (deviceId == Context.DEVICE_ID_DEFAULT || deviceId == Context.DEVICE_ID_INVALID) { + return false; + } + return mVirtualDeviceManagerInternal.getDeviceOwnerUid(deviceId) == callingUid; } private AppOpsManager getAppOpsManager() { diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 745b79209546..5c621208c4db 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1036,7 +1036,7 @@ public class DisplayPolicy { /** * Check if a window can be added to the system. * - * Currently enforces that two window types are singletons per display: + * Currently enforces that these window types are singletons per display: * <ul> * <li>{@link WindowManager.LayoutParams#TYPE_STATUS_BAR}</li> * <li>{@link WindowManager.LayoutParams#TYPE_NOTIFICATION_SHADE}</li> @@ -1058,41 +1058,39 @@ public class DisplayPolicy { ActivityTaskManagerService.enforceTaskPermission("DisplayPolicy"); } + final String systemUiPermission = + mService.isCallerVirtualDeviceOwner(mDisplayContent.getDisplayId(), callingUid) + // Allow virtual device owners to add system windows on their displays. + ? android.Manifest.permission.CREATE_VIRTUAL_DEVICE + : android.Manifest.permission.STATUS_BAR_SERVICE; + switch (attrs.type) { case TYPE_STATUS_BAR: - mContext.enforcePermission( - android.Manifest.permission.STATUS_BAR_SERVICE, callingPid, callingUid, + mContext.enforcePermission(systemUiPermission, callingPid, callingUid, "DisplayPolicy"); if (mStatusBar != null && mStatusBar.isAlive()) { return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON; } break; case TYPE_NOTIFICATION_SHADE: - mContext.enforcePermission( - android.Manifest.permission.STATUS_BAR_SERVICE, callingPid, callingUid, + mContext.enforcePermission(systemUiPermission, callingPid, callingUid, "DisplayPolicy"); - if (mNotificationShade != null) { - if (mNotificationShade.isAlive()) { - return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON; - } + if (mNotificationShade != null && mNotificationShade.isAlive()) { + return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON; } break; case TYPE_NAVIGATION_BAR: - mContext.enforcePermission(android.Manifest.permission.STATUS_BAR_SERVICE, - callingPid, callingUid, "DisplayPolicy"); + mContext.enforcePermission(systemUiPermission, callingPid, callingUid, + "DisplayPolicy"); if (mNavigationBar != null && mNavigationBar.isAlive()) { return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON; } break; case TYPE_NAVIGATION_BAR_PANEL: - mContext.enforcePermission(android.Manifest.permission.STATUS_BAR_SERVICE, - callingPid, callingUid, "DisplayPolicy"); - break; case TYPE_STATUS_BAR_ADDITIONAL: case TYPE_STATUS_BAR_SUB_PANEL: case TYPE_VOICE_INTERACTION_STARTING: - mContext.enforcePermission( - android.Manifest.permission.STATUS_BAR_SERVICE, callingPid, callingUid, + mContext.enforcePermission(systemUiPermission, callingPid, callingUid, "DisplayPolicy"); break; case TYPE_STATUS_BAR_PANEL: @@ -1102,8 +1100,7 @@ public class DisplayPolicy { if (attrs.providedInsets != null) { // Recents component is allowed to add inset types. if (!mService.mAtmService.isCallerRecents(callingUid)) { - mContext.enforcePermission( - android.Manifest.permission.STATUS_BAR_SERVICE, callingPid, callingUid, + mContext.enforcePermission(systemUiPermission, callingPid, callingUid, "DisplayPolicy"); } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 90b9a0452eb1..6b7ba66ca9c9 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1524,7 +1524,7 @@ public class WindowManagerService extends IWindowManager.Stub final boolean isRoundedCornerOverlay = (attrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0; int res = mPolicy.checkAddPermission(attrs.type, isRoundedCornerOverlay, attrs.packageName, - appOp); + appOp, displayId); if (res != ADD_OKAY) { return res; } @@ -10121,6 +10121,23 @@ public class WindowManagerService extends IWindowManager.Stub } } + /** + * Returns whether the given UID is the owner of a virtual device, which the given display + * belongs to. + */ + @Override + public boolean isCallerVirtualDeviceOwner(int displayId, int callingUid) { + if (!android.companion.virtualdevice.flags.Flags.statusBarAndInsets()) { + return false; + } + final long identity = Binder.clearCallingIdentity(); + try { + return mAtmService.mTaskSupervisor.isDeviceOwnerUid(displayId, callingUid); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + @RequiresPermission(ACCESS_SURFACE_FLINGER) @Override public boolean replaceContentOnDisplay(int displayId, SurfaceControl sc) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index fc619677bb56..2b7e9f902ccf 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -11858,7 +11858,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } setBackwardsCompatibleAppRestrictions( caller, packageName, restrictions, caller.getUserHandle()); - } else if (Flags.dmrhSetAppRestrictions()) { + } else { final boolean isRoleHolder; if (who != null) { // DO or PO @@ -11905,15 +11905,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { caller.getUserHandle()); }); } - } else { - Preconditions.checkCallAuthorization((caller.hasAdminComponent() - && (isProfileOwner(caller) || isDefaultDeviceOwner(caller))) - || (caller.hasPackage() && isCallerDelegate(caller, - DELEGATION_APP_RESTRICTIONS))); - mInjector.binderWithCleanCallingIdentity(() -> { - mUserManager.setApplicationRestrictions(packageName, restrictions, - caller.getUserHandle()); - }); } DevicePolicyEventLogger @@ -13244,7 +13235,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return Bundle.EMPTY; } return policies.get(enforcingAdmin).getValue(); - } else if (Flags.dmrhSetAppRestrictions()) { + } else { final boolean isRoleHolder; if (who != null) { // Caller is DO or PO. They cannot call this on parent @@ -13287,19 +13278,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return bundle != null ? bundle : Bundle.EMPTY; }); } - - } else { - Preconditions.checkCallAuthorization((caller.hasAdminComponent() - && (isProfileOwner(caller) || isDefaultDeviceOwner(caller))) - || (caller.hasPackage() && isCallerDelegate(caller, - DELEGATION_APP_RESTRICTIONS))); - return mInjector.binderWithCleanCallingIdentity(() -> { - Bundle bundle = mUserManager.getApplicationRestrictions(packageName, - caller.getUserHandle()); - // if no restrictions were saved, mUserManager.getApplicationRestrictions - // returns null, but DPM method should return an empty Bundle as per JavaDoc - return bundle != null ? bundle : Bundle.EMPTY; - }); } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java index 30c384a03883..5e868a3089f6 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java @@ -38,7 +38,7 @@ public class DisplayOffloadSessionImplTest { private DisplayManagerInternal.DisplayOffloader mDisplayOffloader; @Mock - private DisplayPowerControllerInterface mDisplayPowerController; + private DisplayPowerController mDisplayPowerController; private DisplayOffloadSessionImpl mSession; diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index a7e0ebdd6de1..120cc84193cd 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -118,7 +118,7 @@ public class LocalDisplayAdapterTest { @Mock private DisplayManagerFlags mFlags; @Mock - private DisplayPowerControllerInterface mMockedDisplayPowerController; + private DisplayPowerController mMockedDisplayPowerController; private Handler mHandler; diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt index 8753b251ac98..019ccf93fa11 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt +++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt @@ -48,6 +48,7 @@ import org.mockito.MockitoAnnotations import java.util.LinkedList import java.util.Queue import android.util.ArraySet +import android.view.InputDevice /** * Tests for {@link MouseKeysInterceptor} @@ -68,6 +69,8 @@ class MouseKeysInterceptorTest { } private lateinit var mouseKeysInterceptor: MouseKeysInterceptor + private lateinit var inputDevice: InputDevice + private val clock = OffsettableClock() private val testLooper = TestLooper { clock.now() } private val nextInterceptor = TrackingInterceptor() @@ -98,6 +101,10 @@ class MouseKeysInterceptorTest { testSession = InputManagerGlobal.createTestSession(iInputManager) mockInputManager = InputManager(context) + inputDevice = createInputDevice(DEVICE_ID) + Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)) + .thenReturn(inputDevice) + Mockito.`when`(mockVirtualDeviceManagerInternal.getDeviceIdsForUid(Mockito.anyInt())) .thenReturn(ArraySet(setOf(DEVICE_ID))) LocalServices.removeServiceForTest(VirtualDeviceManagerInternal::class.java) @@ -115,7 +122,8 @@ class MouseKeysInterceptorTest { Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager) - mouseKeysInterceptor = MouseKeysInterceptor(mockAms, testLooper.looper, DISPLAY_ID) + mouseKeysInterceptor = MouseKeysInterceptor(mockAms, mockInputManager, + testLooper.looper, DISPLAY_ID) mouseKeysInterceptor.next = nextInterceptor } @@ -281,6 +289,17 @@ class MouseKeysInterceptorTest { } } + private fun createInputDevice( + deviceId: Int, + generation: Int = -1 + ): InputDevice = + InputDevice.Builder() + .setId(deviceId) + .setName("Device $deviceId") + .setDescriptor("descriptor $deviceId") + .setGeneration(generation) + .build() + private class TrackingInterceptor : BaseEventStreamTransformation() { val events: Queue<KeyEvent> = LinkedList() diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index 07934ea90b7e..e694c0b4afc1 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -188,7 +188,7 @@ public class PhoneWindowManagerTests { .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); int[] outAppOp = new int[1]; assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_WALLPAPER, - /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp)); + /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY)); assertThat(outAppOp[0]).isEqualTo(AppOpsManager.OP_NONE); } @@ -198,7 +198,7 @@ public class PhoneWindowManagerTests { .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); int[] outAppOp = new int[1]; assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_ACCESSIBILITY_OVERLAY, - /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp)); + /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY)); assertThat(outAppOp[0]).isEqualTo(AppOpsManager.OP_CREATE_ACCESSIBILITY_OVERLAY); } @@ -208,7 +208,7 @@ public class PhoneWindowManagerTests { .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); int[] outAppOp = new int[1]; assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_ACCESSIBILITY_OVERLAY, - /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp)); + /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY)); assertThat(outAppOp[0]).isEqualTo(AppOpsManager.OP_NONE); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java index d62c626f9a90..eebb487d16cd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java @@ -60,7 +60,7 @@ class TestWindowManagerPolicy implements WindowManagerPolicy { @Override public int checkAddPermission(int type, boolean isRoundedCornerOverlay, String packageName, - int[] outAppOp) { + int[] outAppOp, int displayId) { return 0; } diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java new file mode 100644 index 000000000000..ad0ef1b3a37f --- /dev/null +++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java @@ -0,0 +1,292 @@ +/* + * Copyright 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.server.input.debug; + +import static android.view.InputDevice.SOURCE_TOUCHSCREEN; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.Rect; +import android.testing.TestableContext; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.cts.input.MotionEventBuilder; +import com.android.cts.input.PointerBuilder; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Build/Install/Run: + * atest TouchpadDebugViewTest + */ +@RunWith(AndroidJUnit4.class) +public class TouchpadDebugViewTest { + private static final int TOUCHPAD_DEVICE_ID = 6; + + private TouchpadDebugView mTouchpadDebugView; + private WindowManager.LayoutParams mWindowLayoutParams; + + @Mock + WindowManager mWindowManager; + + Rect mWindowBounds; + WindowMetrics mWindowMetrics; + TestableContext mTestableContext; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + mTestableContext = new TestableContext(context); + + mTestableContext.addMockSystemService(WindowManager.class, mWindowManager); + + mWindowBounds = new Rect(0, 0, 2560, 1600); + mWindowMetrics = new WindowMetrics(mWindowBounds, new WindowInsets(mWindowBounds), 1.0f); + + when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); + + mTouchpadDebugView = new TouchpadDebugView(mTestableContext, TOUCHPAD_DEVICE_ID); + + mTouchpadDebugView.measure( + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ); + + doAnswer(invocation -> { + mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), + mTouchpadDebugView.getMeasuredHeight()); + return null; + }).when(mWindowManager).addView(any(), any()); + + doAnswer(invocation -> { + mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), + mTouchpadDebugView.getMeasuredHeight()); + return null; + }).when(mWindowManager).updateViewLayout(any(), any()); + + mWindowLayoutParams = mTouchpadDebugView.getWindowLayoutParams(); + mWindowLayoutParams.x = 20; + mWindowLayoutParams.y = 20; + + mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), + mTouchpadDebugView.getMeasuredHeight()); + } + + @Test + public void testDragView() { + // Initial view position relative to screen. + int initialX = mWindowLayoutParams.x; + int initialY = mWindowLayoutParams.y; + + float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; + float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; + + // Simulate ACTION_DOWN event (initial touch). + MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(40f) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionDown); + + verify(mWindowManager, times(0)).updateViewLayout(any(), any()); + + // Simulate ACTION_MOVE event (dragging to the right). + MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f + offsetX) + .y(40f + offsetY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionMove); + + ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = + ArgumentCaptor.forClass(WindowManager.LayoutParams.class); + verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); + + // Verify position after ACTION_MOVE + assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); + assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); + + // Simulate ACTION_UP event (release touch). + MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f + offsetX) + .y(40f + offsetY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionUp); + + assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); + assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); + } + + @Test + public void testDragViewOutOfBounds() { + int initialX = mWindowLayoutParams.x; + int initialY = mWindowLayoutParams.y; + + MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX + 10f) + .y(initialY + 10f) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionDown); + + verify(mWindowManager, times(0)).updateViewLayout(any(), any()); + + // Simulate ACTION_MOVE event (dragging far to the right and bottom, beyond screen bounds) + MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(mWindowBounds.width() + mTouchpadDebugView.getWidth()) + .y(mWindowBounds.height() + mTouchpadDebugView.getHeight()) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionMove); + + ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = + ArgumentCaptor.forClass(WindowManager.LayoutParams.class); + verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); + + // Verify the view has been clamped to the right and bottom edges of the screen + assertEquals(mWindowBounds.width() - mTouchpadDebugView.getWidth(), + mWindowLayoutParamsCaptor.getValue().x); + assertEquals(mWindowBounds.height() - mTouchpadDebugView.getHeight(), + mWindowLayoutParamsCaptor.getValue().y); + + MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(mWindowBounds.width() + mTouchpadDebugView.getWidth()) + .y(mWindowBounds.height() + mTouchpadDebugView.getHeight()) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionUp); + + // Verify the view has been clamped to the right and bottom edges of the screen + assertEquals(mWindowBounds.width() - mTouchpadDebugView.getWidth(), + mWindowLayoutParamsCaptor.getValue().x); + assertEquals(mWindowBounds.height() - mTouchpadDebugView.getHeight(), + mWindowLayoutParamsCaptor.getValue().y); + } + + @Test + public void testSlopOffset() { + int initialX = mWindowLayoutParams.x; + int initialY = mWindowLayoutParams.y; + + float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() / 2.0f; + float offsetY = -(ViewConfiguration.get(mTestableContext).getScaledTouchSlop() / 2.0f); + + MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX) + .y(initialY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionDown); + + MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX + offsetX) + .y(initialY + offsetY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionMove); + + MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX) + .y(initialY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionUp); + + // In this case the updateViewLayout() method wouldn't be called because the drag + // distance hasn't exceeded the slop + verify(mWindowManager, times(0)).updateViewLayout(any(), any()); + } + + @Test + public void testViewReturnsToInitialPositionOnCancel() { + int initialX = mWindowLayoutParams.x; + int initialY = mWindowLayoutParams.y; + + float offsetX = 50; + float offsetY = 50; + + MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX) + .y(initialY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionDown); + + MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX + offsetX) + .y(initialY + offsetY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionMove); + + ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = + ArgumentCaptor.forClass(WindowManager.LayoutParams.class); + verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); + + assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); + assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); + + // Simulate ACTION_CANCEL event (canceling the touch event stream) + MotionEvent actionCancel = new MotionEventBuilder(MotionEvent.ACTION_CANCEL, + SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX + offsetX) + .y(initialY + offsetY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionCancel); + + // Verify the view returns to its initial position + verify(mWindowManager, times(2)).updateViewLayout(any(), + mWindowLayoutParamsCaptor.capture()); + assertEquals(initialX, mWindowLayoutParamsCaptor.getValue().x); + assertEquals(initialY, mWindowLayoutParamsCaptor.getValue().y); + } +} |