summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java113
-rw-r--r--apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp80
-rw-r--r--core/api/system-current.txt4
-rw-r--r--core/java/android/app/ActivityManagerInternal.java3
-rw-r--r--core/java/android/app/IActivityManager.aidl21
-rw-r--r--core/java/android/app/SystemServiceRegistry.java15
-rw-r--r--core/java/android/companion/OWNERS4
-rw-r--r--core/java/android/companion/virtual/IVirtualDevice.aidl5
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceManager.java21
-rw-r--r--core/java/android/content/Context.java14
-rw-r--r--core/java/android/content/pm/PermissionMethod.java2
-rw-r--r--core/java/android/content/pm/PermissionName.java35
-rw-r--r--core/java/android/os/PowerManager.java14
-rw-r--r--core/java/android/os/storage/IStorageManager.aidl7
-rw-r--r--core/java/android/os/storage/StorageManager.java9
-rw-r--r--core/java/android/provider/Settings.java30
-rw-r--r--core/java/android/security/keymaster/KeymasterDefs.java1
-rw-r--r--core/java/com/android/internal/app/ChooserListAdapter.java38
-rw-r--r--core/java/com/android/internal/app/ResolverActivity.java18
-rw-r--r--core/java/com/android/internal/app/ResolverListAdapter.java81
-rw-r--r--core/java/com/android/internal/widget/LockPatternUtils.java13
-rw-r--r--core/java/com/android/internal/widget/LockSettingsInternal.java32
-rw-r--r--core/jni/android_media_AudioVolumeGroups.cpp5
-rw-r--r--core/res/AndroidManifest.xml4
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java8
-rw-r--r--keystore/java/android/security/KeyStore2.java5
-rw-r--r--keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java42
-rw-r--r--keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java11
-rw-r--r--keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java16
-rw-r--r--libs/WindowManager/Shell/res/layout/tv_pip_menu.xml21
-rw-r--r--libs/WindowManager/Shell/res/values-tvdpi/dimen.xml8
-rw-r--r--libs/WindowManager/Shell/res/values/strings_tv.xml4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java64
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java280
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java163
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt10
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt9
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt77
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt26
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt23
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt20
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt26
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt26
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt26
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java2
-rw-r--r--packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java2
-rw-r--r--packages/SystemUI/res-keyguard/values/ids.xml20
-rw-r--r--packages/SystemUI/res/layout/media_ttt_chip.xml4
-rw-r--r--packages/SystemUI/res/values/strings.xml31
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl5
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java11
-rw-r--r--packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt53
-rw-r--r--packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/Flags.java33
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSFragment.java35
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java50
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java53
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java64
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt239
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt179
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java278
-rw-r--r--packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/UserModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt224
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt322
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt75
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt114
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt717
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt150
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt107
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt132
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt122
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt48
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt50
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java45
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java31
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java86
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt34
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt323
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt82
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt204
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt209
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt205
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt394
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt95
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt658
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt220
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt188
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt63
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt16
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt32
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt69
-rw-r--r--services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java3
-rw-r--r--services/companion/java/com/android/server/companion/PackageUtils.java13
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java41
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java10
-rw-r--r--services/core/java/com/android/server/AlarmManagerInternal.java13
-rw-r--r--services/core/java/com/android/server/StorageManagerService.java94
-rw-r--r--services/core/java/com/android/server/SystemClockTime.java174
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java42
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerShellCommand.java2
-rw-r--r--services/core/java/com/android/server/am/UserController.java43
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/AuthResult.java40
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java93
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java157
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java49
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java117
-rw-r--r--services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java4
-rw-r--r--services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java28
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java2
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java25
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodMenuController.java2
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodUtils.java67
-rw-r--r--services/core/java/com/android/server/inputmethod/LocaleUtils.java4
-rw-r--r--services/core/java/com/android/server/inputmethod/SubtypeUtils.java14
-rw-r--r--services/core/java/com/android/server/locksettings/LockSettingsService.java358
-rw-r--r--services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java20
-rw-r--r--services/core/java/com/android/server/pm/ShortcutPackage.java13
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java17
-rw-r--r--services/core/java/com/android/server/power/PowerManagerService.java15
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryStatsImpl.java22
-rw-r--r--services/core/java/com/android/server/timedetector/ConfigurationInternal.java58
-rw-r--r--services/core/java/com/android/server/timedetector/EnvironmentImpl.java40
-rw-r--r--services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java15
-rw-r--r--services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java215
-rw-r--r--services/core/jni/Android.bp1
-rw-r--r--services/core/jni/OWNERS1
-rw-r--r--services/core/jni/com_android_server_SystemClockTime.cpp126
-rw-r--r--services/core/jni/onload.cpp2
-rw-r--r--services/java/com/android/server/SystemServer.java8
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java18
-rw-r--r--services/tests/servicestests/src/com/android/server/am/UserControllerTest.java14
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java94
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java100
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java148
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java56
-rw-r--r--services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java71
-rw-r--r--services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java15
-rw-r--r--services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java25
-rw-r--r--services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java313
-rw-r--r--telephony/common/com/android/internal/telephony/SmsApplication.java154
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java224
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt50
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt9
-rw-r--r--tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java14
-rw-r--r--tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt2
-rw-r--r--tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt4
-rw-r--r--tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt201
-rw-r--r--tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt74
-rw-r--r--tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt119
208 files changed, 9740 insertions, 2290 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 37fb746fa16f..232d3c9aaa53 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -39,6 +39,7 @@ import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROU
import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED;
import static android.os.UserHandle.USER_SYSTEM;
+import static com.android.server.SystemClockTime.TIME_CONFIDENCE_HIGH;
import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH;
import static com.android.server.SystemTimeZone.getTimeZoneId;
import static com.android.server.alarm.Alarm.APP_STANDBY_POLICY_INDEX;
@@ -58,6 +59,8 @@ import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_R
import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_REASON_UNDEFINED;
import android.Manifest;
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.Activity;
@@ -87,7 +90,6 @@ import android.os.BatteryManager;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
-import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -101,7 +103,6 @@ import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.SystemClock;
-import android.os.SystemProperties;
import android.os.ThreadLocalWorkSource;
import android.os.Trace;
import android.os.UserHandle;
@@ -145,6 +146,8 @@ import com.android.server.DeviceIdleInternal;
import com.android.server.EventLogTags;
import com.android.server.JobSchedulerBackgroundThread;
import com.android.server.LocalServices;
+import com.android.server.SystemClockTime;
+import com.android.server.SystemClockTime.TimeConfidence;
import com.android.server.SystemService;
import com.android.server.SystemServiceManager;
import com.android.server.SystemTimeZone;
@@ -1417,7 +1420,7 @@ public class AlarmManagerService extends SystemService {
private long convertToElapsed(long when, int type) {
if (isRtc(type)) {
- when -= mInjector.getCurrentTimeMillis() - mInjector.getElapsedRealtime();
+ when -= mInjector.getCurrentTimeMillis() - mInjector.getElapsedRealtimeMillis();
}
return when;
}
@@ -1577,7 +1580,7 @@ public class AlarmManagerService extends SystemService {
alarmsToDeliver = alarmsForUid;
mPendingBackgroundAlarms.remove(uid);
}
- deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtime());
+ deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtimeMillis());
}
/**
@@ -1594,7 +1597,8 @@ public class AlarmManagerService extends SystemService {
mPendingBackgroundAlarms, alarmsToDeliver, this::isBackgroundRestricted);
if (alarmsToDeliver.size() > 0) {
- deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtime());
+ deliverPendingBackgroundAlarmsLocked(
+ alarmsToDeliver, mInjector.getElapsedRealtimeMillis());
}
}
@@ -1908,17 +1912,8 @@ public class AlarmManagerService extends SystemService {
mInjector.syncKernelTimeZoneOffset();
}
- // Ensure that we're booting with a halfway sensible current time. Use the
- // most recent of Build.TIME, the root file system's timestamp, and the
- // value of the ro.build.date.utc system property (which is in seconds).
- final long systemBuildTime = Long.max(
- 1000L * SystemProperties.getLong("ro.build.date.utc", -1L),
- Long.max(Environment.getRootDirectory().lastModified(), Build.TIME));
- if (mInjector.getCurrentTimeMillis() < systemBuildTime) {
- Slog.i(TAG, "Current time only " + mInjector.getCurrentTimeMillis()
- + ", advancing to build time " + systemBuildTime);
- mInjector.setKernelTime(systemBuildTime);
- }
+ // Ensure that we're booting with a halfway sensible current time.
+ mInjector.initializeTimeIfRequired();
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
// Determine SysUI's uid
@@ -2133,21 +2128,18 @@ public class AlarmManagerService extends SystemService {
}
}
- boolean setTimeImpl(long millis) {
- if (!mInjector.isAlarmDriverPresent()) {
- Slog.w(TAG, "Not setting time since no alarm driver is available.");
- return false;
- }
-
+ boolean setTimeImpl(
+ @CurrentTimeMillisLong long newSystemClockTimeMillis, @TimeConfidence int confidence,
+ @NonNull String logMsg) {
synchronized (mLock) {
- final long currentTimeMillis = mInjector.getCurrentTimeMillis();
- mInjector.setKernelTime(millis);
+ final long oldSystemClockTimeMillis = mInjector.getCurrentTimeMillis();
+ mInjector.setCurrentTimeMillis(newSystemClockTimeMillis, confidence, logMsg);
if (KERNEL_TIME_ZONE_SYNC_ENABLED) {
// Changing the time may cross a DST transition; sync the kernel offset if needed.
final TimeZone timeZone = TimeZone.getTimeZone(SystemTimeZone.getTimeZoneId());
- final int currentTzOffset = timeZone.getOffset(currentTimeMillis);
- final int newTzOffset = timeZone.getOffset(millis);
+ final int currentTzOffset = timeZone.getOffset(oldSystemClockTimeMillis);
+ final int newTzOffset = timeZone.getOffset(newSystemClockTimeMillis);
if (currentTzOffset != newTzOffset) {
Slog.i(TAG, "Timezone offset has changed, updating kernel timezone");
mInjector.setKernelTimeZoneOffset(newTzOffset);
@@ -2260,7 +2252,7 @@ public class AlarmManagerService extends SystemService {
triggerAtTime = 0;
}
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
final long nominalTrigger = convertToElapsed(triggerAtTime, type);
// Try to prevent spamming by making sure apps aren't firing alarms in the immediate future
final long minTrigger = nowElapsed
@@ -2383,7 +2375,7 @@ public class AlarmManagerService extends SystemService {
// No need to fuzz as this is already earlier than the coming wake-from-idle.
return changedBeforeFuzz;
}
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
final long futurity = upcomingWakeFromIdle - nowElapsed;
if (futurity <= mConstants.MIN_DEVICE_IDLE_FUZZ) {
@@ -2405,7 +2397,7 @@ public class AlarmManagerService extends SystemService {
* @return {@code true} if the alarm delivery time was updated.
*/
private boolean adjustDeliveryTimeBasedOnBatterySaver(Alarm alarm) {
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
if (isExemptFromBatterySaver(alarm)) {
return false;
}
@@ -2472,7 +2464,7 @@ public class AlarmManagerService extends SystemService {
* @return {@code true} if the alarm delivery time was updated.
*/
private boolean adjustDeliveryTimeBasedOnDeviceIdle(Alarm alarm) {
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
if (mPendingIdleUntil == null || mPendingIdleUntil == alarm) {
return alarm.setPolicyElapsed(DEVICE_IDLE_POLICY_INDEX, nowElapsed);
}
@@ -2527,7 +2519,7 @@ public class AlarmManagerService extends SystemService {
* adjustments made in this call.
*/
private boolean adjustDeliveryTimeBasedOnBucketLocked(Alarm alarm) {
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
if (mConstants.USE_TARE_POLICY || isExemptFromAppStandby(alarm) || mAppStandbyParole) {
return alarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, nowElapsed);
}
@@ -2587,7 +2579,7 @@ public class AlarmManagerService extends SystemService {
* adjustments made in this call.
*/
private boolean adjustDeliveryTimeBasedOnTareLocked(Alarm alarm) {
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
if (!mConstants.USE_TARE_POLICY
|| isExemptFromTare(alarm) || hasEnoughWealthLocked(alarm)) {
return alarm.setPolicyElapsed(TARE_POLICY_INDEX, nowElapsed);
@@ -2643,7 +2635,7 @@ public class AlarmManagerService extends SystemService {
ent.pkg = a.sourcePackage;
ent.tag = a.statsTag;
ent.op = "START IDLE";
- ent.elapsedRealtime = mInjector.getElapsedRealtime();
+ ent.elapsedRealtime = mInjector.getElapsedRealtimeMillis();
ent.argRealtime = a.getWhenElapsed();
mAllowWhileIdleDispatches.add(ent);
}
@@ -2719,6 +2711,13 @@ public class AlarmManagerService extends SystemService {
}
@Override
+ public void setTime(
+ @CurrentTimeMillisLong long unixEpochTimeMillis, int confidence,
+ String logMsg) {
+ setTimeImpl(unixEpochTimeMillis, confidence, logMsg);
+ }
+
+ @Override
public void registerInFlightListener(InFlightListener callback) {
synchronized (mLock) {
mInFlightListeners.add(callback);
@@ -2987,12 +2986,16 @@ public class AlarmManagerService extends SystemService {
}
@Override
- public boolean setTime(long millis) {
+ public boolean setTime(@CurrentTimeMillisLong long millis) {
getContext().enforceCallingOrSelfPermission(
"android.permission.SET_TIME",
"setTime");
- return setTimeImpl(millis);
+ // The public API (and the shell command that also uses this method) have no concept
+ // of confidence, but since the time should come either from apps working on behalf of
+ // the user or a developer, confidence is assumed "high".
+ final int timeConfidence = TIME_CONFIDENCE_HIGH;
+ return setTimeImpl(millis, timeConfidence, "AlarmManager.setTime() called");
}
@Override
@@ -3142,7 +3145,7 @@ public class AlarmManagerService extends SystemService {
pw.println();
}
- final long nowELAPSED = mInjector.getElapsedRealtime();
+ final long nowELAPSED = mInjector.getElapsedRealtimeMillis();
final long nowUPTIME = SystemClock.uptimeMillis();
final long nowRTC = mInjector.getCurrentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@@ -3618,7 +3621,7 @@ public class AlarmManagerService extends SystemService {
synchronized (mLock) {
final long nowRTC = mInjector.getCurrentTimeMillis();
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
proto.write(AlarmManagerServiceDumpProto.CURRENT_TIME, nowRTC);
proto.write(AlarmManagerServiceDumpProto.ELAPSED_REALTIME, nowElapsed);
proto.write(AlarmManagerServiceDumpProto.LAST_TIME_CHANGE_CLOCK_TIME,
@@ -3956,7 +3959,7 @@ public class AlarmManagerService extends SystemService {
void rescheduleKernelAlarmsLocked() {
// Schedule the next upcoming wakeup alarm. If there is a deliverable batch
// prior to that which contains no wakeups, we schedule that as well.
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
long nextNonWakeup = 0;
if (mAlarmStore.size() > 0) {
final long firstWakeup = mAlarmStore.getNextWakeupDeliveryTime();
@@ -4069,7 +4072,7 @@ public class AlarmManagerService extends SystemService {
@GuardedBy("mLock")
private void removeAlarmsInternalLocked(Predicate<Alarm> whichAlarms, int reason) {
final long nowRtc = mInjector.getCurrentTimeMillis();
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
final ArrayList<Alarm> removedAlarms = mAlarmStore.remove(whichAlarms);
final boolean removedFromStore = !removedAlarms.isEmpty();
@@ -4208,7 +4211,7 @@ public class AlarmManagerService extends SystemService {
void interactiveStateChangedLocked(boolean interactive) {
if (mInteractive != interactive) {
mInteractive = interactive;
- final long nowELAPSED = mInjector.getElapsedRealtime();
+ final long nowELAPSED = mInjector.getElapsedRealtimeMillis();
if (interactive) {
if (mPendingNonWakeupAlarms.size() > 0) {
final long thisDelayTime = nowELAPSED - mStartCurrentDelayTime;
@@ -4310,7 +4313,6 @@ public class AlarmManagerService extends SystemService {
private static native void close(long nativeData);
private static native int set(long nativeData, int type, long seconds, long nanoseconds);
private static native int waitForAlarm(long nativeData);
- private static native int setKernelTime(long nativeData, long millis);
/*
* b/246256335: The @Keep ensures that the native definition is kept even when the optimizer can
@@ -4357,7 +4359,7 @@ public class AlarmManagerService extends SystemService {
ent.pkg = alarm.sourcePackage;
ent.tag = alarm.statsTag;
ent.op = "END IDLE";
- ent.elapsedRealtime = mInjector.getElapsedRealtime();
+ ent.elapsedRealtime = mInjector.getElapsedRealtimeMillis();
ent.argRealtime = alarm.getWhenElapsed();
mAllowWhileIdleDispatches.add(ent);
}
@@ -4602,20 +4604,27 @@ public class AlarmManagerService extends SystemService {
setKernelTimeZoneOffset(utcOffsetMillis);
}
- void setKernelTime(long millis) {
- if (mNativeData != 0) {
- AlarmManagerService.setKernelTime(mNativeData, millis);
- }
+ void initializeTimeIfRequired() {
+ SystemClockTime.initializeIfRequired();
+ }
+
+ void setCurrentTimeMillis(
+ @CurrentTimeMillisLong long unixEpochMillis,
+ @TimeConfidence int confidence,
+ @NonNull String logMsg) {
+ SystemClockTime.setTimeAndConfidence(unixEpochMillis, confidence, logMsg);
}
void close() {
AlarmManagerService.close(mNativeData);
}
- long getElapsedRealtime() {
+ @ElapsedRealtimeLong
+ long getElapsedRealtimeMillis() {
return SystemClock.elapsedRealtime();
}
+ @CurrentTimeMillisLong
long getCurrentTimeMillis() {
return System.currentTimeMillis();
}
@@ -4665,7 +4674,7 @@ public class AlarmManagerService extends SystemService {
while (true) {
int result = mInjector.waitForAlarm();
final long nowRTC = mInjector.getCurrentTimeMillis();
- final long nowELAPSED = mInjector.getElapsedRealtime();
+ final long nowELAPSED = mInjector.getElapsedRealtimeMillis();
synchronized (mLock) {
mLastWakeup = nowELAPSED;
}
@@ -4913,7 +4922,7 @@ public class AlarmManagerService extends SystemService {
// this way, so WAKE_UP alarms will be delivered only when the device is awake.
ArrayList<Alarm> triggerList = new ArrayList<Alarm>();
synchronized (mLock) {
- final long nowELAPSED = mInjector.getElapsedRealtime();
+ final long nowELAPSED = mInjector.getElapsedRealtimeMillis();
triggerAlarmsLocked(triggerList, nowELAPSED);
updateNextAlarmClockLocked();
}
@@ -5090,7 +5099,7 @@ public class AlarmManagerService extends SystemService {
flags |= mConstants.TIME_TICK_ALLOWED_WHILE_IDLE ? FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED
: 0;
- setImpl(ELAPSED_REALTIME, mInjector.getElapsedRealtime() + tickEventDelay, 0,
+ setImpl(ELAPSED_REALTIME, mInjector.getElapsedRealtimeMillis() + tickEventDelay, 0,
0, null, mTimeTickTrigger, TIME_TICK_TAG, flags, workSource, null,
Process.myUid(), "android", null, EXACT_ALLOW_REASON_ALLOW_LIST);
@@ -5277,7 +5286,7 @@ public class AlarmManagerService extends SystemService {
}
synchronized (mLock) {
mTemporaryQuotaReserve.replenishQuota(packageName, userId, quotaBump,
- mInjector.getElapsedRealtime());
+ mInjector.getElapsedRealtimeMillis());
}
mHandler.obtainMessage(AlarmHandler.TEMPORARY_QUOTA_CHANGED, userId, -1,
packageName).sendToTarget();
@@ -5439,7 +5448,7 @@ public class AlarmManagerService extends SystemService {
}
private void updateStatsLocked(InFlight inflight) {
- final long nowELAPSED = mInjector.getElapsedRealtime();
+ final long nowELAPSED = mInjector.getElapsedRealtimeMillis();
BroadcastStats bs = inflight.mBroadcastStats;
bs.nesting--;
if (bs.nesting <= 0) {
diff --git a/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp b/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp
index 8f9e187a7a93..b2ed4d47adf4 100644
--- a/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp
+++ b/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp
@@ -76,19 +76,17 @@ typedef std::array<int, N_ANDROID_TIMERFDS> TimerFds;
class AlarmImpl
{
public:
- AlarmImpl(const TimerFds &fds, int epollfd, const std::string &rtc_dev)
- : fds{fds}, epollfd{epollfd}, rtc_dev{rtc_dev} {}
+ AlarmImpl(const TimerFds &fds, int epollfd)
+ : fds{fds}, epollfd{epollfd} {}
~AlarmImpl();
int set(int type, struct timespec *ts);
- int setTime(struct timeval *tv);
int waitForAlarm();
int getTime(int type, struct itimerspec *spec);
private:
const TimerFds fds;
const int epollfd;
- std::string rtc_dev;
};
AlarmImpl::~AlarmImpl()
@@ -131,43 +129,6 @@ int AlarmImpl::getTime(int type, struct itimerspec *spec)
return timerfd_gettime(fds[type], spec);
}
-int AlarmImpl::setTime(struct timeval *tv)
-{
- if (settimeofday(tv, NULL) == -1) {
- ALOGV("settimeofday() failed: %s", strerror(errno));
- return -1;
- }
-
- android::base::unique_fd fd{open(rtc_dev.c_str(), O_RDWR)};
- if (!fd.ok()) {
- ALOGE("Unable to open %s: %s", rtc_dev.c_str(), strerror(errno));
- return -1;
- }
-
- struct tm tm;
- if (!gmtime_r(&tv->tv_sec, &tm)) {
- ALOGV("gmtime_r() failed: %s", strerror(errno));
- return -1;
- }
-
- struct rtc_time rtc = {};
- rtc.tm_sec = tm.tm_sec;
- rtc.tm_min = tm.tm_min;
- rtc.tm_hour = tm.tm_hour;
- rtc.tm_mday = tm.tm_mday;
- rtc.tm_mon = tm.tm_mon;
- rtc.tm_year = tm.tm_year;
- rtc.tm_wday = tm.tm_wday;
- rtc.tm_yday = tm.tm_yday;
- rtc.tm_isdst = tm.tm_isdst;
- if (ioctl(fd, RTC_SET_TIME, &rtc) == -1) {
- ALOGV("RTC_SET_TIME ioctl failed: %s", strerror(errno));
- return -1;
- }
-
- return 0;
-}
-
int AlarmImpl::waitForAlarm()
{
epoll_event events[N_ANDROID_TIMERFDS];
@@ -198,28 +159,6 @@ int AlarmImpl::waitForAlarm()
return result;
}
-static jint android_server_alarm_AlarmManagerService_setKernelTime(JNIEnv*, jobject, jlong nativeData, jlong millis)
-{
- AlarmImpl *impl = reinterpret_cast<AlarmImpl *>(nativeData);
-
- if (millis <= 0 || millis / 1000LL >= std::numeric_limits<time_t>::max()) {
- return -1;
- }
-
- struct timeval tv;
- tv.tv_sec = (millis / 1000LL);
- tv.tv_usec = ((millis % 1000LL) * 1000LL);
-
- ALOGD("Setting time of day to sec=%ld", tv.tv_sec);
-
- int ret = impl->setTime(&tv);
- if (ret < 0) {
- ALOGW("Unable to set rtc to %ld: %s", tv.tv_sec, strerror(errno));
- ret = -1;
- }
- return ret;
-}
-
static jint android_server_alarm_AlarmManagerService_setKernelTimezone(JNIEnv*, jobject, jlong, jint minswest)
{
struct timezone tz;
@@ -287,19 +226,7 @@ static jlong android_server_alarm_AlarmManagerService_init(JNIEnv*, jobject)
}
}
- // Find the wall clock RTC. We expect this always to be /dev/rtc0, but
- // check the /dev/rtc symlink first so that legacy devices that don't use
- // rtc0 can add a symlink rather than need to carry a local patch to this
- // code.
- //
- // TODO: if you're reading this in a world where all devices are using the
- // GKI, you can remove the readlink and just assume /dev/rtc0.
- std::string dev_rtc;
- if (!android::base::Readlink("/dev/rtc", &dev_rtc)) {
- dev_rtc = "/dev/rtc0";
- }
-
- std::unique_ptr<AlarmImpl> alarm{new AlarmImpl(fds, epollfd, dev_rtc)};
+ std::unique_ptr<AlarmImpl> alarm{new AlarmImpl(fds, epollfd)};
for (size_t i = 0; i < fds.size(); i++) {
epoll_event event;
@@ -392,7 +319,6 @@ static const JNINativeMethod sMethods[] = {
{"close", "(J)V", (void*)android_server_alarm_AlarmManagerService_close},
{"set", "(JIJJ)I", (void*)android_server_alarm_AlarmManagerService_set},
{"waitForAlarm", "(J)I", (void*)android_server_alarm_AlarmManagerService_waitForAlarm},
- {"setKernelTime", "(JJ)I", (void*)android_server_alarm_AlarmManagerService_setKernelTime},
{"setKernelTimezone", "(JI)I", (void*)android_server_alarm_AlarmManagerService_setKernelTimezone},
{"getNextAlarm", "(JI)J", (void*)android_server_alarm_AlarmManagerService_getNextAlarm},
};
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index f221eca45337..134b71a49272 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -167,6 +167,7 @@ package android {
field public static final String MANAGE_CONTENT_SUGGESTIONS = "android.permission.MANAGE_CONTENT_SUGGESTIONS";
field public static final String MANAGE_DEBUGGING = "android.permission.MANAGE_DEBUGGING";
field public static final String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS";
+ field public static final String MANAGE_DEVICE_POLICY_APP_EXEMPTIONS = "android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS";
field public static final String MANAGE_ETHERNET_NETWORKS = "android.permission.MANAGE_ETHERNET_NETWORKS";
field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION";
field public static final String MANAGE_GAME_ACTIVITY = "android.permission.MANAGE_GAME_ACTIVITY";
@@ -2789,6 +2790,8 @@ package android.companion.virtual {
public final class VirtualDeviceManager {
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.VirtualDeviceManager.VirtualDevice createVirtualDevice(int, @NonNull android.companion.virtual.VirtualDeviceParams);
+ field public static final int DEFAULT_DEVICE_ID = 0; // 0x0
+ field public static final int INVALID_DEVICE_ID = -1; // 0xffffffff
field public static final int LAUNCH_FAILURE_NO_ACTIVITY = 2; // 0x2
field public static final int LAUNCH_FAILURE_PENDING_INTENT_CANCELED = 1; // 0x1
field public static final int LAUNCH_SUCCESS = 0; // 0x0
@@ -2808,6 +2811,7 @@ package android.companion.virtual {
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+ method public int getDeviceId();
method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean);
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 626b7d3cd5b1..8da9631a23b0 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -31,6 +31,7 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ActivityPresentationInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PermissionMethod;
+import android.content.pm.PermissionName;
import android.content.pm.UserInfo;
import android.net.Uri;
import android.os.Bundle;
@@ -294,7 +295,7 @@ public abstract class ActivityManagerInternal {
/** Checks if the calling binder pid as the permission. */
@PermissionMethod
- public abstract void enforceCallingPermission(String permission, String func);
+ public abstract void enforceCallingPermission(@PermissionName String permission, String func);
/** Returns the current user id. */
public abstract int getCurrentUserId();
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index b4abd3c69482..6404a1f59135 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -524,9 +524,30 @@ interface IActivityManager {
@UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
void suppressResizeConfigChanges(boolean suppress);
+
+ /**
+ * @deprecated Use {@link #unlockUser2(int, IProgressListener)} instead, since the token and
+ * secret arguments no longer do anything. This method still exists only because it is marked
+ * with {@code @UnsupportedAppUsage}, so it might not be safe to remove it or change its
+ * signature.
+ */
@UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
boolean unlockUser(int userid, in byte[] token, in byte[] secret,
in IProgressListener listener);
+
+ /**
+ * Tries to unlock the given user.
+ * <p>
+ * This will succeed only if the user's CE storage key is already unlocked or if the user
+ * doesn't have a lockscreen credential set.
+ *
+ * @param userId The ID of the user to unlock.
+ * @param listener An optional progress listener.
+ *
+ * @return true if the user was successfully unlocked, otherwise false.
+ */
+ boolean unlockUser2(int userId, in IProgressListener listener);
+
void killPackageDependents(in String packageName, int userId);
void makePackageIdle(String packageName, int userId);
int getMemoryTrimLevel();
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index d5b85cde4926..4ddfdb603e73 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -81,6 +81,8 @@ import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.IDomainVerificationManager;
import android.content.res.Resources;
import android.content.rollback.RollbackManagerFrameworkInitializer;
+import android.credentials.CredentialManager;
+import android.credentials.ICredentialManager;
import android.debug.AdbManager;
import android.debug.IAdbManager;
import android.graphics.fonts.FontManager;
@@ -1138,6 +1140,19 @@ public final class SystemServiceRegistry {
return new AutofillManager(ctx.getOuterContext(), service);
}});
+ registerService(Context.CREDENTIAL_SERVICE, CredentialManager.class,
+ new CachedServiceFetcher<CredentialManager>() {
+ @Override
+ public CredentialManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getService(Context.CREDENTIAL_SERVICE);
+ ICredentialManager service = ICredentialManager.Stub.asInterface(b);
+ if (service != null) {
+ return new CredentialManager(ctx.getOuterContext(), service);
+ }
+ return null;
+ }});
+
registerService(Context.MUSIC_RECOGNITION_SERVICE, MusicRecognitionManager.class,
new CachedServiceFetcher<MusicRecognitionManager>() {
@Override
diff --git a/core/java/android/companion/OWNERS b/core/java/android/companion/OWNERS
index 004f66caed7b..0348fe2776fe 100644
--- a/core/java/android/companion/OWNERS
+++ b/core/java/android/companion/OWNERS
@@ -1,5 +1,3 @@
-ewol@google.com
evanxinchen@google.com
guojing@google.com
-svetoslavganov@google.com
-sergeynv@google.com \ No newline at end of file
+raphk@google.com \ No newline at end of file
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 9c99da52d962..e7f191665302 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -43,6 +43,11 @@ interface IVirtualDevice {
int getAssociationId();
/**
+ * Returns the unique device ID for this virtual device.
+ */
+ int getDeviceId();
+
+ /**
* Closes the virtual device and frees all associated resources.
*/
void close();
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index fadfa5cd68a5..08bee2552a1f 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -78,6 +78,16 @@ public final class VirtualDeviceManager {
| DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
| DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
+ /**
+ * The default device ID, which is the ID of the primary (non-virtual) device.
+ */
+ public static final int DEFAULT_DEVICE_ID = 0;
+
+ /**
+ * Invalid device ID.
+ */
+ public static final int INVALID_DEVICE_ID = -1;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
@@ -204,6 +214,17 @@ public final class VirtualDeviceManager {
}
/**
+ * Returns the unique ID of this virtual device.
+ */
+ public int getDeviceId() {
+ try {
+ return mVirtualDevice.getDeviceId();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Launches a given pending intent on the give display ID.
*
* @param displayId The display to launch the pending intent on. This display must be
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 0d73c1375b03..cb5a99fcc811 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -52,6 +52,7 @@ import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionMethod;
+import android.content.pm.PermissionName;
import android.content.res.AssetManager;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
@@ -6088,7 +6089,8 @@ public abstract class Context {
@CheckResult(suggest="#enforcePermission(String,int,int,String)")
@PackageManager.PermissionResult
@PermissionMethod
- public abstract int checkPermission(@NonNull String permission, int pid, int uid);
+ public abstract int checkPermission(
+ @NonNull @PermissionName String permission, int pid, int uid);
/** @hide */
@SuppressWarnings("HiddenAbstractMethod")
@@ -6121,7 +6123,7 @@ public abstract class Context {
@CheckResult(suggest="#enforceCallingPermission(String,String)")
@PackageManager.PermissionResult
@PermissionMethod
- public abstract int checkCallingPermission(@NonNull String permission);
+ public abstract int checkCallingPermission(@NonNull @PermissionName String permission);
/**
* Determine whether the calling process of an IPC <em>or you</em> have been
@@ -6142,7 +6144,7 @@ public abstract class Context {
@CheckResult(suggest="#enforceCallingOrSelfPermission(String,String)")
@PackageManager.PermissionResult
@PermissionMethod
- public abstract int checkCallingOrSelfPermission(@NonNull String permission);
+ public abstract int checkCallingOrSelfPermission(@NonNull @PermissionName String permission);
/**
* Determine whether <em>you</em> have been granted a particular permission.
@@ -6172,7 +6174,7 @@ public abstract class Context {
*/
@PermissionMethod
public abstract void enforcePermission(
- @NonNull String permission, int pid, int uid, @Nullable String message);
+ @NonNull @PermissionName String permission, int pid, int uid, @Nullable String message);
/**
* If the calling process of an IPC you are handling has not been
@@ -6194,7 +6196,7 @@ public abstract class Context {
*/
@PermissionMethod
public abstract void enforceCallingPermission(
- @NonNull String permission, @Nullable String message);
+ @NonNull @PermissionName String permission, @Nullable String message);
/**
* If neither you nor the calling process of an IPC you are
@@ -6211,7 +6213,7 @@ public abstract class Context {
*/
@PermissionMethod
public abstract void enforceCallingOrSelfPermission(
- @NonNull String permission, @Nullable String message);
+ @NonNull @PermissionName String permission, @Nullable String message);
/**
* Grant permission to access a specific Uri to another package, regardless
diff --git a/core/java/android/content/pm/PermissionMethod.java b/core/java/android/content/pm/PermissionMethod.java
index 021b2e1cd8f1..ba97342c5e3e 100644
--- a/core/java/android/content/pm/PermissionMethod.java
+++ b/core/java/android/content/pm/PermissionMethod.java
@@ -26,7 +26,7 @@ import java.lang.annotation.Target;
* Documents that the subject method's job is to look
* up whether the provided or calling uid/pid has the requested permission.
*
- * Methods should either return `void`, but potentially throw {@link SecurityException},
+ * <p>Methods should either return `void`, but potentially throw {@link SecurityException},
* or return {@link android.content.pm.PackageManager.PermissionResult} `int`.
*
* @hide
diff --git a/core/java/android/content/pm/PermissionName.java b/core/java/android/content/pm/PermissionName.java
new file mode 100644
index 000000000000..719e13be05cc
--- /dev/null
+++ b/core/java/android/content/pm/PermissionName.java
@@ -0,0 +1,35 @@
+/*
+ * 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 android.content.pm;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that the annotated {@link String} represents a permission name.
+ *
+ * @hide
+ */
+@Retention(CLASS)
+@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
+public @interface PermissionName {}
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index c0e2864d1993..9bc7ffdef26e 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -2913,6 +2913,7 @@ public final class PowerManager {
private int mFlags;
@UnsupportedAppUsage
private String mTag;
+ private int mTagHash;
private final String mPackageName;
private final IBinder mToken;
private int mInternalCount;
@@ -2921,7 +2922,6 @@ public final class PowerManager {
private boolean mHeld;
private WorkSource mWorkSource;
private String mHistoryTag;
- private final String mTraceName;
private final int mDisplayId;
private WakeLockStateListener mListener;
private IWakeLockCallback mCallback;
@@ -2931,9 +2931,9 @@ public final class PowerManager {
WakeLock(int flags, String tag, String packageName, int displayId) {
mFlags = flags;
mTag = tag;
+ mTagHash = mTag.hashCode();
mPackageName = packageName;
mToken = new Binder();
- mTraceName = "WakeLock (" + mTag + ")";
mDisplayId = displayId;
}
@@ -2942,7 +2942,8 @@ public final class PowerManager {
synchronized (mToken) {
if (mHeld) {
Log.wtf(TAG, "WakeLock finalized while still held: " + mTag);
- Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+ "WakeLocks", mTagHash);
try {
mService.releaseWakeLock(mToken, 0);
} catch (RemoteException e) {
@@ -3012,7 +3013,8 @@ public final class PowerManager {
// should immediately acquire the wake lock once again despite never having
// been explicitly released by the keyguard.
mHandler.removeCallbacks(mReleaser);
- Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_POWER,
+ "WakeLocks", mTag, mTagHash);
try {
mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource,
mHistoryTag, mDisplayId, mCallback);
@@ -3060,7 +3062,8 @@ public final class PowerManager {
if (!mRefCounted || mInternalCount == 0) {
mHandler.removeCallbacks(mReleaser);
if (mHeld) {
- Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+ "WakeLocks", mTagHash);
try {
mService.releaseWakeLock(mToken, flags);
} catch (RemoteException e) {
@@ -3137,6 +3140,7 @@ public final class PowerManager {
/** @hide */
public void setTag(String tag) {
mTag = tag;
+ mTagHash = mTag.hashCode();
}
/** @hide */
diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl
index df0bee7e1f2d..bc52744078ea 100644
--- a/core/java/android/os/storage/IStorageManager.aidl
+++ b/core/java/android/os/storage/IStorageManager.aidl
@@ -137,6 +137,7 @@ interface IStorageManager {
void createUserKey(int userId, int serialNumber, boolean ephemeral) = 61;
@EnforcePermission("STORAGE_INTERNAL")
void destroyUserKey(int userId) = 62;
+ @EnforcePermission("STORAGE_INTERNAL")
void unlockUserKey(int userId, int serialNumber, in byte[] secret) = 63;
@EnforcePermission("STORAGE_INTERNAL")
void lockUserKey(int userId) = 64;
@@ -146,9 +147,7 @@ interface IStorageManager {
@EnforcePermission("STORAGE_INTERNAL")
void destroyUserStorage(in String volumeUuid, int userId, int flags) = 67;
@EnforcePermission("STORAGE_INTERNAL")
- void addUserKeyAuth(int userId, int serialNumber, in byte[] secret) = 70;
- @EnforcePermission("STORAGE_INTERNAL")
- void fixateNewestUserKeyAuth(int userId) = 71;
+ void setUserKeyProtection(int userId, in byte[] secret) = 70;
@EnforcePermission("MOUNT_FORMAT_FILESYSTEMS")
void fstrim(int flags, IVoldTaskListener listener) = 72;
AppFuseMount mountProxyFileDescriptorBridge() = 73;
@@ -165,8 +164,6 @@ interface IStorageManager {
@EnforcePermission("MOUNT_FORMAT_FILESYSTEMS")
boolean needsCheckpoint() = 86;
void abortChanges(in String message, boolean retry) = 87;
- @EnforcePermission("STORAGE_INTERNAL")
- void clearUserKeyAuth(int userId, int serialNumber, in byte[] secret) = 88;
void fixupAppDir(in String path) = 89;
void disableAppDataIsolation(in String pkgName, int pid, int userId) = 90;
PendingIntent getManageSpaceActivityIntent(in String packageName, int requestCode) = 91;
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index c1606e89645e..38ac98425ae6 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1606,15 +1606,6 @@ public class StorageManager {
}
/** {@hide} */
- public void unlockUserKey(int userId, int serialNumber, byte[] secret) {
- try {
- mStorageManager.unlockUserKey(userId, serialNumber, secret);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /** {@hide} */
public void lockUserKey(int userId) {
try {
mStorageManager.lockUserKey(userId);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index cab6acbfe2b7..00633a2a9a26 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -393,6 +393,21 @@ public final class Settings {
"android.settings.ACCESSIBILITY_DETAILS_SETTINGS";
/**
+ * Activity Action: Show settings to allow configuration of accessibility color and motion.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_ACCESSIBILITY_COLOR_MOTION_SETTINGS =
+ "android.settings.ACCESSIBILITY_COLOR_MOTION_SETTINGS";
+
+ /**
* Activity Action: Show settings to allow configuration of Reduce Bright Colors.
* <p>
* In some cases, a matching Activity may not exist, so ensure you
@@ -438,6 +453,21 @@ public final class Settings {
"android.settings.COLOR_INVERSION_SETTINGS";
/**
+ * Activity Action: Show settings to allow configuration of text reading.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_TEXT_READING_SETTINGS =
+ "android.settings.TEXT_READING_SETTINGS";
+
+ /**
* Activity Action: Show settings to control access to usage information.
* <p>
* In some cases, a matching Activity may not exist, so ensure you
diff --git a/core/java/android/security/keymaster/KeymasterDefs.java b/core/java/android/security/keymaster/KeymasterDefs.java
index 8efc5eb6b6ff..e720f1ab1523 100644
--- a/core/java/android/security/keymaster/KeymasterDefs.java
+++ b/core/java/android/security/keymaster/KeymasterDefs.java
@@ -65,6 +65,7 @@ public final class KeymasterDefs {
public static final int KM_TAG_PADDING = Tag.PADDING; // KM_ENUM_REP | 6;
public static final int KM_TAG_CALLER_NONCE = Tag.CALLER_NONCE; // KM_BOOL | 7;
public static final int KM_TAG_MIN_MAC_LENGTH = Tag.MIN_MAC_LENGTH; // KM_UINT | 8;
+ public static final int KM_TAG_EC_CURVE = Tag.EC_CURVE; // KM_ENUM | 10;
public static final int KM_TAG_RSA_PUBLIC_EXPONENT = Tag.RSA_PUBLIC_EXPONENT; // KM_ULONG | 200;
public static final int KM_TAG_INCLUDE_UNIQUE_ID = Tag.INCLUDE_UNIQUE_ID; // KM_BOOL | 202;
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 1ec5325623ec..4f74ca72b4aa 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -86,7 +86,6 @@ public class ChooserListAdapter extends ResolverListAdapter {
private final ChooserActivityLogger mChooserActivityLogger;
private int mNumShortcutResults = 0;
- private Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
private boolean mApplySharingAppLimits;
// Reserve spots for incoming direct share targets by adding placeholders
@@ -265,31 +264,20 @@ public class ChooserListAdapter extends ResolverListAdapter {
return;
}
- if (!(info instanceof DisplayResolveInfo)) {
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
- holder.bindIcon(info);
-
- if (info instanceof SelectableTargetInfo) {
- // direct share targets should append the application name for a better readout
- DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
- CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
- CharSequence extendedInfo = info.getExtendedInfo();
- String contentDescription = String.join(" ", info.getDisplayLabel(),
- extendedInfo != null ? extendedInfo : "", appName);
- holder.updateContentDescription(contentDescription);
- }
- } else {
+ holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
+ holder.bindIcon(info);
+ if (info instanceof SelectableTargetInfo) {
+ // direct share targets should append the application name for a better readout
+ DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
+ CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
+ CharSequence extendedInfo = info.getExtendedInfo();
+ String contentDescription = String.join(" ", info.getDisplayLabel(),
+ extendedInfo != null ? extendedInfo : "", appName);
+ holder.updateContentDescription(contentDescription);
+ } else if (info instanceof DisplayResolveInfo) {
DisplayResolveInfo dri = (DisplayResolveInfo) info;
- holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel());
- LoadIconTask task = mIconLoaders.get(dri);
- if (task == null) {
- task = new LoadIconTask(dri, holder);
- mIconLoaders.put(dri, task);
- task.execute();
- } else {
- // The holder was potentially changed as the underlying items were
- // reshuffled, so reset the target holder
- task.setViewHolder(holder);
+ if (!dri.hasDisplayIcon()) {
+ loadIcon(dri);
}
}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index c8bc204bd9aa..822393ff221e 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -55,6 +55,7 @@ import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Insets;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -1475,14 +1476,21 @@ public class ResolverActivity extends Activity implements
mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0);
boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
- ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter();
- DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
+ final ResolverListAdapter inactiveAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+ final DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
// Load the icon asynchronously
ImageView icon = findViewById(R.id.icon);
- ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask(
- otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon));
- iconTask.execute();
+ inactiveAdapter.new LoadIconTask(otherProfileResolveInfo) {
+ @Override
+ protected void onPostExecute(Drawable drawable) {
+ if (!isDestroyed()) {
+ otherProfileResolveInfo.setDisplayIcon(drawable);
+ new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
+ }
+ }
+ }.execute();
((TextView) findViewById(R.id.open_cross_profile)).setText(
getResources().getString(
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 66fff5c13ab7..f6075b008f72 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -58,7 +58,10 @@ import com.android.internal.app.chooser.DisplayResolveInfo;
import com.android.internal.app.chooser.TargetInfo;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class ResolverListAdapter extends BaseAdapter {
private static final String TAG = "ResolverListAdapter";
@@ -87,6 +90,8 @@ public class ResolverListAdapter extends BaseAdapter {
private Runnable mPostListReadyRunnable;
private final boolean mIsAudioCaptureDevice;
private boolean mIsTabLoaded;
+ private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
+ private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>();
public ResolverListAdapter(Context context, List<Intent> payloadIntents,
Intent[] initialIntents, List<ResolveInfo> rList,
@@ -636,26 +641,47 @@ public class ResolverListAdapter extends BaseAdapter {
if (info == null) {
holder.icon.setImageDrawable(
mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+ holder.bindLabel("", "", false);
return;
}
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayLabel()) {
- getLoadLabelTask((DisplayResolveInfo) info, holder).execute();
- } else {
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
+ if (info instanceof DisplayResolveInfo) {
+ DisplayResolveInfo dri = (DisplayResolveInfo) info;
+ boolean hasLabel = dri.hasDisplayLabel();
+ holder.bindLabel(
+ dri.getDisplayLabel(),
+ dri.getExtendedInfo(),
+ hasLabel && alwaysShowSubLabel());
+ holder.bindIcon(info);
+ if (!hasLabel) {
+ loadLabel(dri);
+ }
+ if (!dri.hasDisplayIcon()) {
+ loadIcon(dri);
+ }
}
+ }
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayIcon()) {
- new LoadIconTask((DisplayResolveInfo) info, holder).execute();
- } else {
- holder.bindIcon(info);
+ protected final void loadIcon(DisplayResolveInfo info) {
+ LoadIconTask task = mIconLoaders.get(info);
+ if (task == null) {
+ task = new LoadIconTask((DisplayResolveInfo) info);
+ mIconLoaders.put(info, task);
+ task.execute();
+ }
+ }
+
+ private void loadLabel(DisplayResolveInfo info) {
+ LoadLabelTask task = mLabelLoaders.get(info);
+ if (task == null) {
+ task = createLoadLabelTask(info);
+ mLabelLoaders.put(info, task);
+ task.execute();
}
}
- protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
- return new LoadLabelTask(info, holder);
+ protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+ return new LoadLabelTask(info);
}
public void onDestroy() {
@@ -666,6 +692,16 @@ public class ResolverListAdapter extends BaseAdapter {
if (mResolverListController != null) {
mResolverListController.destroy();
}
+ cancelTasks(mIconLoaders.values());
+ cancelTasks(mLabelLoaders.values());
+ mIconLoaders.clear();
+ mLabelLoaders.clear();
+ }
+
+ private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) {
+ for (T task: tasks) {
+ task.cancel(false);
+ }
}
private static ColorMatrixColorFilter getSuspendedColorMatrix() {
@@ -883,11 +919,9 @@ public class ResolverListAdapter extends BaseAdapter {
protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
private final DisplayResolveInfo mDisplayResolveInfo;
- private final ViewHolder mHolder;
- protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) {
+ protected LoadLabelTask(DisplayResolveInfo dri) {
mDisplayResolveInfo = dri;
- mHolder = holder;
}
@Override
@@ -925,21 +959,22 @@ public class ResolverListAdapter extends BaseAdapter {
@Override
protected void onPostExecute(CharSequence[] result) {
+ if (mDisplayResolveInfo.hasDisplayLabel()) {
+ return;
+ }
mDisplayResolveInfo.setDisplayLabel(result[0]);
mDisplayResolveInfo.setExtendedInfo(result[1]);
- mHolder.bindLabel(result[0], result[1], alwaysShowSubLabel());
+ notifyDataSetChanged();
}
}
class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
protected final DisplayResolveInfo mDisplayResolveInfo;
private final ResolveInfo mResolveInfo;
- private ViewHolder mHolder;
- LoadIconTask(DisplayResolveInfo dri, ViewHolder holder) {
+ LoadIconTask(DisplayResolveInfo dri) {
mDisplayResolveInfo = dri;
mResolveInfo = dri.getResolveInfo();
- mHolder = holder;
}
@Override
@@ -953,17 +988,9 @@ public class ResolverListAdapter extends BaseAdapter {
mResolverListCommunicator.updateProfileViewButton();
} else if (!mDisplayResolveInfo.hasDisplayIcon()) {
mDisplayResolveInfo.setDisplayIcon(d);
- mHolder.bindIcon(mDisplayResolveInfo);
- // Notify in case view is already bound to resolve the race conditions on
- // low end devices
notifyDataSetChanged();
}
}
-
- public void setViewHolder(ViewHolder holder) {
- mHolder = holder;
- mHolder.bindIcon(mDisplayResolveInfo);
- }
}
/**
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 65c2d00fda06..55a26fee407f 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -26,6 +26,7 @@ import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.PropertyInvalidatedCache;
import android.app.admin.DevicePolicyManager;
import android.app.admin.PasswordMetrics;
@@ -1784,4 +1785,16 @@ public class LockPatternUtils {
re.rethrowFromSystemServer();
}
}
+
+ public void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
+ getLockSettingsInternal().unlockUserKeyIfUnsecured(userId);
+ }
+
+ public void createNewUser(@UserIdInt int userId, int userSerialNumber) {
+ getLockSettingsInternal().createNewUser(userId, userSerialNumber);
+ }
+
+ public void removeUser(@UserIdInt int userId) {
+ getLockSettingsInternal().removeUser(userId);
+ }
}
diff --git a/core/java/com/android/internal/widget/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java
index 0a2c18f83fef..5b08bb1f42d3 100644
--- a/core/java/com/android/internal/widget/LockSettingsInternal.java
+++ b/core/java/com/android/internal/widget/LockSettingsInternal.java
@@ -18,6 +18,7 @@ package com.android.internal.widget;
import android.annotation.IntDef;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.admin.PasswordMetrics;
import java.lang.annotation.Retention;
@@ -53,6 +54,37 @@ public abstract class LockSettingsInternal {
// TODO(b/183140900) split store escrow key errors into detailed ones.
/**
+ * Unlocks the credential-encrypted storage for the given user if the user is not secured, i.e.
+ * doesn't have an LSKF.
+ * <p>
+ * This doesn't throw an exception on failure; whether the storage has been unlocked can be
+ * determined by {@link StorageManager#isUserKeyUnlocked()}.
+ *
+ * @param userId the ID of the user whose storage to unlock
+ */
+ public abstract void unlockUserKeyIfUnsecured(@UserIdInt int userId);
+
+ /**
+ * Creates the locksettings state for a new user.
+ * <p>
+ * This includes creating a synthetic password and protecting it with an empty LSKF.
+ *
+ * @param userId the ID of the new user
+ * @param userSerialNumber the serial number of the new user
+ */
+ public abstract void createNewUser(@UserIdInt int userId, int userSerialNumber);
+
+ /**
+ * Removes the locksettings state for the given user.
+ * <p>
+ * This includes removing the user's synthetic password and any protectors that are protecting
+ * it.
+ *
+ * @param userId the ID of the user being removed
+ */
+ public abstract void removeUser(@UserIdInt int userId);
+
+ /**
* Create an escrow token for the current user, which can later be used to unlock FBE
* or change user password.
*
diff --git a/core/jni/android_media_AudioVolumeGroups.cpp b/core/jni/android_media_AudioVolumeGroups.cpp
index 7098451901c4..1252e89935e5 100644
--- a/core/jni/android_media_AudioVolumeGroups.cpp
+++ b/core/jni/android_media_AudioVolumeGroups.cpp
@@ -94,6 +94,11 @@ static jint convertAudioVolumeGroupsFromNative(
for (size_t j = 0; j < static_cast<size_t>(numAttributes); j++) {
auto attributes = group.getAudioAttributes()[j];
+ // Native & Java audio attributes default initializers are not aligned for the source.
+ // Given the volume group class concerns only playback, this field must be equal to the
+ // default java initializer.
+ attributes.source = AUDIO_SOURCE_INVALID;
+
jStatus = JNIAudioAttributeHelper::nativeToJava(env, &jAudioAttribute, attributes);
if (jStatus != AUDIO_JAVA_SUCCESS) {
goto exit;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ded1b898d857..f0b1b2aa7e3d 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3051,6 +3051,10 @@
<permission android:name="android.permission.QUERY_ADMIN_POLICY"
android:protectionLevel="signature|role" />
+ <!-- @SystemApi @hide Allows an application to exempt apps from platform restrictions.-->
+ <permission android:name="android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS"
+ android:protectionLevel="signature|role" />
+
<!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
<permission android:name="android.permission.PROVISION_DEMO_DEVICE"
android:protectionLevel="signature|setup" />
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
index 56a70708b6df..2861428ece4d 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
@@ -46,14 +46,14 @@ public class ResolverWrapperAdapter extends ResolverListAdapter {
}
@Override
- protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
- return new LoadLabelWrapperTask(info, holder);
+ protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+ return new LoadLabelWrapperTask(info);
}
class LoadLabelWrapperTask extends LoadLabelTask {
- protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) {
- super(dri, holder);
+ protected LoadLabelWrapperTask(DisplayResolveInfo dri) {
+ super(dri);
}
@Override
diff --git a/keystore/java/android/security/KeyStore2.java b/keystore/java/android/security/KeyStore2.java
index c2cd6ffc622c..f507d76d9e5c 100644
--- a/keystore/java/android/security/KeyStore2.java
+++ b/keystore/java/android/security/KeyStore2.java
@@ -32,6 +32,7 @@ import android.system.keystore2.ResponseCode;
import android.util.Log;
import java.util.Calendar;
+import java.util.Objects;
/**
* @hide This should not be made public in its present form because it
@@ -137,13 +138,13 @@ public class KeyStore2 {
return new KeyStore2();
}
- private synchronized IKeystoreService getService(boolean retryLookup) {
+ @NonNull private synchronized IKeystoreService getService(boolean retryLookup) {
if (mBinder == null || retryLookup) {
mBinder = IKeystoreService.Stub.asInterface(ServiceManager
.getService(KEYSTORE2_SERVICE_NAME));
Binder.allowBlocking(mBinder.asBinder());
}
- return mBinder;
+ return Objects.requireNonNull(mBinder);
}
void delete(KeyDescriptor descriptor) throws KeyStoreException {
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java
index b631999c2c54..4e73bd9d3c82 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java
@@ -18,13 +18,19 @@ package android.security.keystore2;
import android.annotation.NonNull;
import android.security.KeyStoreSecurityLevel;
+import android.security.keymaster.KeymasterDefs;
import android.security.keystore.KeyProperties;
+import android.system.keystore2.Authorization;
import android.system.keystore2.KeyDescriptor;
import android.system.keystore2.KeyMetadata;
+import java.security.AlgorithmParameters;
+import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
+import java.security.spec.InvalidParameterSpecException;
/**
* {@link ECPublicKey} backed by keystore.
@@ -56,11 +62,45 @@ public class AndroidKeyStoreECPublicKey extends AndroidKeyStorePublicKey impleme
}
}
+ private static String getEcCurveFromKeymaster(int ecCurve) {
+ switch (ecCurve) {
+ case android.hardware.security.keymint.EcCurve.P_224:
+ return "secp224r1";
+ case android.hardware.security.keymint.EcCurve.P_256:
+ return "secp256r1";
+ case android.hardware.security.keymint.EcCurve.P_384:
+ return "secp384r1";
+ case android.hardware.security.keymint.EcCurve.P_521:
+ return "secp521r1";
+ }
+ return "";
+ }
+
+ private ECParameterSpec getCurveSpec(String name)
+ throws NoSuchAlgorithmException, InvalidParameterSpecException {
+ AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
+ parameters.init(new ECGenParameterSpec(name));
+ return parameters.getParameterSpec(ECParameterSpec.class);
+ }
+
@Override
public AndroidKeyStorePrivateKey getPrivateKey() {
+ ECParameterSpec params = mParams;
+ for (Authorization a : getAuthorizations()) {
+ try {
+ if (a.keyParameter.tag == KeymasterDefs.KM_TAG_EC_CURVE) {
+ params = getCurveSpec(getEcCurveFromKeymaster(
+ a.keyParameter.value.getEcCurve()));
+ break;
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to parse EC curve "
+ + a.keyParameter.value.getEcCurve());
+ }
+ }
return new AndroidKeyStoreECPrivateKey(
getUserKeyDescriptor(), getKeyIdDescriptor().nspace, getAuthorizations(),
- getSecurityLevel(), mParams);
+ getSecurityLevel(), params);
}
@Override
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
index b1338d164055..4caa47f2078b 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
@@ -31,6 +31,8 @@ import java.security.NoSuchAlgorithmException;
import java.security.ProviderException;
import java.security.PublicKey;
import java.security.SecureRandom;
+import java.security.interfaces.ECKey;
+import java.security.interfaces.XECKey;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.List;
@@ -132,6 +134,15 @@ public class AndroidKeyStoreKeyAgreementSpi extends KeyAgreementSpi
throw new InvalidKeyException("key == null");
} else if (!(key instanceof PublicKey)) {
throw new InvalidKeyException("Only public keys supported. Key: " + key);
+ } else if (!(mKey instanceof ECKey && key instanceof ECKey)
+ && !(mKey instanceof XECKey && key instanceof XECKey)) {
+ throw new InvalidKeyException(
+ "Public and Private key should be of the same type:");
+ } else if (mKey instanceof ECKey
+ && !((ECKey) key).getParams().getCurve()
+ .equals(((ECKey) mKey).getParams().getCurve())) {
+ throw new InvalidKeyException(
+ "Public and Private key parameters should be same.");
} else if (!lastPhase) {
throw new IllegalStateException(
"Only one other party supported. lastPhase must be set to true.");
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
index 42589640d2b7..e392c8dcca93 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
@@ -22,16 +22,18 @@ import android.system.keystore2.Authorization;
import android.system.keystore2.KeyDescriptor;
import java.security.PrivateKey;
-import java.security.interfaces.EdECKey;
+import java.security.interfaces.XECPrivateKey;
import java.security.spec.NamedParameterSpec;
+import java.util.Optional;
/**
* X25519 Private Key backed by Keystore.
- * instance of {@link PrivateKey} and {@link EdECKey}
+ * instance of {@link PrivateKey} and {@link XECPrivateKey}
*
* @hide
*/
-public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey implements EdECKey {
+public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey
+ implements XECPrivateKey {
public AndroidKeyStoreXDHPrivateKey(
@NonNull KeyDescriptor descriptor, long keyId,
@NonNull Authorization[] authorizations,
@@ -44,4 +46,12 @@ public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey impl
public NamedParameterSpec getParams() {
return NamedParameterSpec.X25519;
}
+
+ @Override
+ public Optional<byte[]> getScalar() {
+ /* An empty Optional if the scalar cannot be extracted (e.g. if the provider is a hardware
+ * token and the private key is not allowed to leave the crypto boundary).
+ */
+ return Optional.empty();
+ }
}
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
index afd3aac9b461..70755e6cc3cf 100644
--- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
@@ -119,7 +119,7 @@
<!-- Temporarily extending the background to show an edu text hint for opening the menu -->
<FrameLayout
- android:id="@+id/tv_pip_menu_edu_text_container"
+ android:id="@+id/tv_pip_menu_edu_text_drawer_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/tv_pip"
@@ -127,23 +127,8 @@
android:layout_alignStart="@+id/tv_pip"
android:layout_alignEnd="@+id/tv_pip"
android:background="@color/tv_pip_menu_background"
- android:clipChildren="true">
-
- <TextView
- android:id="@+id/tv_pip_menu_edu_text"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/pip_menu_edu_text_view_height"
- android:layout_gravity="bottom|center"
- android:gravity="center"
- android:clickable="false"
- android:paddingBottom="@dimen/pip_menu_border_width"
- android:text="@string/pip_edu_text"
- android:singleLine="true"
- android:ellipsize="marquee"
- android:marqueeRepeatLimit="1"
- android:scrollHorizontally="true"
- android:textAppearance="@style/TvPipEduText"/>
- </FrameLayout>
+ android:paddingBottom="@dimen/pip_menu_border_width"
+ android:paddingTop="@dimen/pip_menu_border_width"/>
<!-- Frame around the PiP content + edu text hint - used to highlight open menu -->
<View
diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
index b45b9ec0c457..9833a88a1c0a 100644
--- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
+++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
@@ -41,8 +41,10 @@
<dimen name="pip_menu_edu_text_view_height">24dp</dimen>
<dimen name="pip_menu_edu_text_home_icon">9sp</dimen>
<dimen name="pip_menu_edu_text_home_icon_outline">14sp</dimen>
- <integer name="pip_edu_text_show_duration_ms">10500</integer>
- <integer name="pip_edu_text_window_exit_animation_duration_ms">1000</integer>
- <integer name="pip_edu_text_view_exit_animation_duration_ms">300</integer>
+ <integer name="pip_edu_text_scroll_times">2</integer>
+ <integer name="pip_edu_text_non_scroll_show_duration">10500</integer>
+ <integer name="pip_edu_text_start_scroll_delay">2000</integer>
+ <integer name="pip_edu_text_window_exit_animation_duration">1000</integer>
+ <integer name="pip_edu_text_view_exit_animation_duration">300</integer>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml
index 2b7a13eac6ca..8f806cf56c9b 100644
--- a/libs/WindowManager/Shell/res/values/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values/strings_tv.xml
@@ -42,8 +42,8 @@
<!-- Educative text instructing the user to double press the HOME button to access the pip
controls menu [CHAR LIMIT=50] -->
- <string name="pip_edu_text"> Double press <annotation icon="home_icon"> HOME </annotation> for
- controls </string>
+ <string name="pip_edu_text">Double press <annotation icon="home_icon">HOME</annotation> for
+ controls</string>
<!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] -->
<string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index c4accde23d93..3b735fce3fa9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -69,7 +69,6 @@ import com.android.wm.shell.floating.FloatingTasksController;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
-import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
@@ -192,25 +191,6 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
- Context context,
- ShellInit shellInit,
- ShellCommandHandler shellCommandHandler,
- SyncTransactionQueue syncTransactionQueue,
- DisplayController displayController,
- DisplayInsetsController displayInsetsController,
- Optional<UnfoldAnimationController> unfoldAnimationController,
- Optional<RecentTasksController> recentTasksOptional,
- @ShellMainThread ShellExecutor mainExecutor,
- @ShellMainThread Handler mainHandler
- ) {
- return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
- syncTransactionQueue, displayController, displayInsetsController,
- unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
- }
-
- @WMSingleton
- @Provides
static CompatUIController provideCompatUIController(Context context,
ShellInit shellInit,
ShellController shellController,
@@ -782,7 +762,6 @@ public abstract class WMShellBaseModule {
DisplayInsetsController displayInsetsController,
DragAndDropController dragAndDropController,
ShellTaskOrganizer shellTaskOrganizer,
- KidsModeTaskOrganizer kidsModeTaskOrganizer,
Optional<BubbleController> bubblesOptional,
Optional<SplitScreenController> splitScreenOptional,
Optional<Pip> pipOptional,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 37a50b611039..47b665970d28 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -56,6 +56,7 @@ import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
+import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
@@ -620,6 +621,28 @@ public abstract class WMShellModule {
}
//
+ // Kids mode
+ //
+ @WMSingleton
+ @Provides
+ static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
+ Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ SyncTransactionQueue syncTransactionQueue,
+ DisplayController displayController,
+ DisplayInsetsController displayInsetsController,
+ Optional<UnfoldAnimationController> unfoldAnimationController,
+ Optional<RecentTasksController> recentTasksOptional,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellMainThread Handler mainHandler
+ ) {
+ return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
+ syncTransactionQueue, displayController, displayInsetsController,
+ unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
+ }
+
+ //
// Misc
//
@@ -630,6 +653,7 @@ public abstract class WMShellModule {
@Provides
static Object provideIndependentShellComponentsToCreate(
DefaultMixedHandler defaultMixedHandler,
+ KidsModeTaskOrganizer kidsModeTaskOrganizer,
Optional<DesktopModeController> desktopModeController) {
return new Object();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
index 4def15db2f52..2624ee536b58 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
@@ -59,10 +59,15 @@ interface IPip {
/**
* Sets listener to get pinned stack animation callbacks.
*/
- oneway void setPinnedStackAnimationListener(IPipAnimationListener listener) = 3;
+ oneway void setPipAnimationListener(IPipAnimationListener listener) = 3;
/**
* Sets the shelf height and visibility.
*/
oneway void setShelfHeight(boolean visible, int shelfHeight) = 4;
+
+ /**
+ * Sets the next pip animation type to be the alpha animation.
+ */
+ oneway void setPipAnimationTypeToAlpha() = 5;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index c06881ae6ad7..72b9dd37ac7d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -51,15 +51,6 @@ public interface Pip {
}
/**
- * Sets both shelf visibility and its height.
- *
- * @param visible visibility of shelf.
- * @param height to specify the height for shelf.
- */
- default void setShelfHeight(boolean visible, int height) {
- }
-
- /**
* Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed.
*
* @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()}
@@ -68,14 +59,6 @@ public interface Pip {
default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {}
/**
- * Set the pinned stack with {@link PipAnimationController.AnimationType}
- *
- * @param animationType The pre-defined {@link PipAnimationController.AnimationType}
- */
- default void setPinnedStackAnimationType(int animationType) {
- }
-
- /**
* Called when showing Pip menu.
*/
default void showPictureInPictureMenu() {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index af47666efa5a..3345b1b2d0e8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -23,6 +23,7 @@ import static android.view.WindowManager.INPUT_CONSUMER_PIP;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN;
@@ -1065,13 +1066,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
@Override
- public void setShelfHeight(boolean visible, int height) {
- mMainExecutor.execute(() -> {
- PipController.this.setShelfHeight(visible, height);
- });
- }
-
- @Override
public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
mMainExecutor.execute(() -> {
PipController.this.setOnIsInPipStateChangedListener(callback);
@@ -1079,13 +1073,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
@Override
- public void setPinnedStackAnimationType(int animationType) {
- mMainExecutor.execute(() -> {
- PipController.this.setPinnedStackAnimationType(animationType);
- });
- }
-
- @Override
public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) {
mMainExecutor.execute(() -> {
mPipBoundsState.addPipExclusionBoundsChangeCallback(listener);
@@ -1178,8 +1165,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
@Override
- public void setPinnedStackAnimationListener(IPipAnimationListener listener) {
- executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener",
+ public void setPipAnimationListener(IPipAnimationListener listener) {
+ executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener",
(controller) -> {
if (listener != null) {
mListener.register(listener);
@@ -1188,5 +1175,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
});
}
+
+ @Override
+ public void setPipAnimationTypeToAlpha() {
+ executeRemoteCallWithTaskPermission(mController, "setPipAnimationTypeToAlpha",
+ (controller) -> {
+ controller.setPinnedStackAnimationType(ANIM_TYPE_ALPHA);
+ });
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
index acc0caf95e35..d7d335b856be 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java
@@ -43,16 +43,16 @@ public class PipDoubleTapHelper {
* <p>MAX - maximum allowed screen size</p>
*/
@IntDef(value = {
- SIZE_SPEC_CUSTOM,
SIZE_SPEC_DEFAULT,
- SIZE_SPEC_MAX
+ SIZE_SPEC_MAX,
+ SIZE_SPEC_CUSTOM
})
@Retention(RetentionPolicy.SOURCE)
@interface PipSizeSpec {}
- static final int SIZE_SPEC_CUSTOM = 2;
static final int SIZE_SPEC_DEFAULT = 0;
static final int SIZE_SPEC_MAX = 1;
+ static final int SIZE_SPEC_CUSTOM = 2;
/**
* Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 85a544b2e8de..3e8de454bcff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -127,7 +127,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private int mPipForceCloseDelay;
private int mResizeAnimationDuration;
- private int mEduTextWindowExitAnimationDurationMs;
+ private int mEduTextWindowExitAnimationDuration;
public static Pip create(
Context context,
@@ -240,12 +240,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState));
- if (isPipShown()) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: > closing Pip.", TAG);
- closePip();
- }
-
loadConfigurations();
mPipNotificationController.onConfigurationChanged(mContext);
mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
@@ -377,10 +371,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
@Override
- public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) {
- mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds,
+ public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
+ mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
animationDuration, rect -> mTvPipMenuController.updateExpansionState());
- mTvPipMenuController.onPipTransitionStarted(newTargetBounds);
+ mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
}
/**
@@ -417,7 +411,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
@Override
public void closeEduText() {
- updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
+ updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false);
}
private void registerSessionListenerForCurrentUser() {
@@ -459,27 +453,30 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
@Override
- public void onPipTransitionStarted(int direction, Rect pipBounds) {
+ public void onPipTransitionStarted(int direction, Rect currentPipBounds) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState));
- mTvPipMenuController.notifyPipAnimating(true);
+ "%s: onPipTransition_Started(), state=%s, direction=%d",
+ TAG, stateToName(mState), direction);
}
@Override
public void onPipTransitionCanceled(int direction) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
- mTvPipMenuController.notifyPipAnimating(false);
+ mTvPipMenuController.onPipTransitionFinished(
+ PipAnimationController.isInPipDirection(direction));
}
@Override
public void onPipTransitionFinished(int direction) {
- if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) {
+ final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction);
+ if (enterPipTransition && mState == STATE_NO_PIP) {
setState(STATE_PIP);
}
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState));
- mTvPipMenuController.notifyPipAnimating(false);
+ "%s: onPipTransition_Finished(), state=%s, direction=%d",
+ TAG, stateToName(mState), direction);
+ mTvPipMenuController.onPipTransitionFinished(enterPipTransition);
}
private void setState(@State int state) {
@@ -493,8 +490,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
final Resources res = mContext.getResources();
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
- mEduTextWindowExitAnimationDurationMs =
- res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
+ mEduTextWindowExitAnimationDuration =
+ res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration);
}
private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index 176fdfee2512..ab7edbfaa4ca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -60,9 +60,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
private final SystemWindows mSystemWindows;
private final TvPipBoundsState mTvPipBoundsState;
private final Handler mMainHandler;
- private final int mPipMenuBorderWidth;
- private final int mPipEduTextShowDurationMs;
- private final int mPipEduTextHeight;
private Delegate mDelegate;
private SurfaceControl mLeash;
@@ -85,8 +82,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
RectF mTmpDestinationRectF = new RectF();
Matrix mMoveTransform = new Matrix();
- private final Runnable mCloseEduTextRunnable = this::closeEduText;
-
public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState,
SystemWindows systemWindows, PipMediaController pipMediaController,
Handler mainHandler) {
@@ -109,12 +104,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
pipMediaController.addActionListener(this::onMediaActionsChanged);
- mPipEduTextShowDurationMs = context.getResources()
- .getInteger(R.integer.pip_edu_text_show_duration_ms);
- mPipEduTextHeight = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
- mPipMenuBorderWidth = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_border_width);
}
void setDelegate(Delegate delegate) {
@@ -152,15 +141,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
attachPipBackgroundView();
attachPipMenuView();
- mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth,
- -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth));
- mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight));
- mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs);
+ int pipEduTextHeight = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
+ int pipMenuBorderWidth = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.pip_menu_border_width);
+ mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth,
+ -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
+ mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight));
}
private void attachPipMenuView() {
- mPipMenuView = new TvPipMenuView(mContext);
- mPipMenuView.setListener(this);
+ mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this);
setUpViewSurfaceZOrder(mPipMenuView, 1);
addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE);
maybeUpdateMenuViewActions();
@@ -192,11 +183,15 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
0 /* height */), 0 /* displayId */, SHELL_ROOT_LAYER_PIP);
}
- void notifyPipAnimating(boolean animating) {
- mPipMenuView.setEduTextActive(!animating);
- if (!animating) {
- mPipMenuView.onPipTransitionFinished(mTvPipBoundsState.isTvPipExpanded());
- }
+ void onPipTransitionFinished(boolean enterTransition) {
+ // There is a race between when this is called and when the last frame of the pip transition
+ // is drawn. To ensure that view updates are applied only when the animation has fully drawn
+ // and the menu view has been fully remeasured and relaid out, we add a small delay here by
+ // posting on the handler.
+ mMainHandler.post(() -> {
+ mPipMenuView.onPipTransitionFinished(
+ enterTransition, mTvPipBoundsState.isTvPipExpanded());
+ });
}
void showMovementMenuOnly() {
@@ -219,7 +214,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
if (mPipMenuView == null) {
return;
}
- maybeCloseEduText();
maybeUpdateMenuViewActions();
updateExpansionState();
@@ -232,25 +226,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
mPipMenuView.updateBounds(mTvPipBoundsState.getBounds());
}
- void onPipTransitionStarted(Rect finishBounds) {
+ void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
if (mPipMenuView != null) {
- mPipMenuView.onPipTransitionStarted(finishBounds);
- }
- }
-
- private void maybeCloseEduText() {
- if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) {
- mMainHandler.removeCallbacks(mCloseEduTextRunnable);
- mCloseEduTextRunnable.run();
+ mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds);
}
}
- private void closeEduText() {
- mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
- mPipMenuView.hideEduText();
- mDelegate.closeEduText();
- }
-
void updateGravity(int gravity) {
mPipMenuView.showMovementHints(gravity);
}
@@ -332,7 +313,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
@Override
public void detach() {
closeMenu();
- mMainHandler.removeCallbacks(mCloseEduTextRunnable);
detachPipMenu();
mLeash = null;
}
@@ -578,6 +558,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
mDelegate.togglePipExpansion();
}
+ @Override
+ public void onCloseEduText() {
+ mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
+ mDelegate.closeEduText();
+ }
+
interface Delegate {
void movePipToFullscreen();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
new file mode 100644
index 000000000000..6eef22562caa
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
@@ -0,0 +1,280 @@
+/*
+ * 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.wm.shell.pip.tv;
+
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.CENTER;
+import static android.view.View.GONE;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.text.Annotation;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+
+import java.util.Arrays;
+
+/**
+ * The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu.
+ * It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same
+ * width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might
+ * not be enough space to fit the whole educational text in the available space. In such cases we
+ * apply a marquee animation to the TextView inside the drawer.
+ *
+ * The drawer is shown temporarily giving the user enough time to read it, after which it slides
+ * shut. We show the text for a duration calculated based on whether the text is marqueed or not.
+ */
+class TvPipMenuEduTextDrawer extends FrameLayout {
+ private static final String TAG = "TvPipMenuEduTextDrawer";
+
+ private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND
+ private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY
+ private final float mMarqueeAnimSpeed; // pixels per ms
+
+ private final Runnable mCloseDrawerRunnable = this::closeDrawer;
+ private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText;
+
+ private final Handler mMainHandler;
+ private final Listener mListener;
+ private final TextView mEduTextView;
+
+ TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) {
+ super(context, null, 0, 0);
+
+ mListener = listener;
+ mMainHandler = mainHandler;
+
+ // Taken from TextView.Marquee calculation
+ mMarqueeAnimSpeed =
+ (MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f;
+
+ mEduTextView = new TextView(mContext);
+ setupDrawer();
+ }
+
+ private void setupDrawer() {
+ final int eduTextHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.pip_menu_edu_text_view_height);
+ final int marqueeRepeatLimit = mContext.getResources()
+ .getInteger(R.integer.pip_edu_text_scroll_times);
+
+ mEduTextView.setLayoutParams(
+ new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER));
+ mEduTextView.setGravity(CENTER);
+ mEduTextView.setClickable(false);
+ mEduTextView.setText(createEduTextString());
+ mEduTextView.setSingleLine();
+ mEduTextView.setTextAppearance(R.style.TvPipEduText);
+ mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
+ mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit);
+ mEduTextView.setHorizontallyScrolling(true);
+ mEduTextView.setHorizontalFadingEdgeEnabled(true);
+ mEduTextView.setSelected(false);
+ addView(mEduTextView);
+
+ setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER));
+ setClipChildren(true);
+ }
+
+ /**
+ * Initializes the edu text. Should only be called once when the PiP is entered
+ */
+ void init() {
+ ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG);
+ scheduleLifecycleEvents();
+ }
+
+ private void scheduleLifecycleEvents() {
+ final int startScrollDelay = mContext.getResources().getInteger(
+ R.integer.pip_edu_text_start_scroll_delay);
+ if (isEduTextMarqueed()) {
+ mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay);
+ }
+ mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration());
+ mEduTextView.getViewTreeObserver().addOnWindowAttachListener(
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ }
+
+ @Override
+ public void onWindowDetached() {
+ mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this);
+ mMainHandler.removeCallbacks(mStartScrollEduTextRunnable);
+ mMainHandler.removeCallbacks(mCloseDrawerRunnable);
+ }
+ });
+ }
+
+ private int getEduTextShowDuration() {
+ int eduTextShowDuration;
+ if (isEduTextMarqueed()) {
+ // Calculate the time it takes to fully scroll the text once: time = distance / speed
+ final float singleMarqueeDuration =
+ getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed;
+ // The TextView adds a delay between each marquee repetition. Take that into account
+ final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY;
+ // Finally, multiply by the number of times we repeat the marquee animation
+ eduTextShowDuration =
+ (int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit();
+ } else {
+ eduTextShowDuration = mContext.getResources()
+ .getInteger(R.integer.pip_edu_text_non_scroll_show_duration);
+ }
+
+ ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d",
+ TAG, eduTextShowDuration);
+ return eduTextShowDuration;
+ }
+
+ /**
+ * Returns true if the edu text width is bigger than the width of the text view, which indicates
+ * that the edu text will be marqueed
+ */
+ private boolean isEduTextMarqueed() {
+ final int availableWidth = (int) mEduTextView.getWidth()
+ - mEduTextView.getCompoundPaddingLeft()
+ - mEduTextView.getCompoundPaddingRight();
+ return availableWidth < getEduTextWidth();
+ }
+
+ /**
+ * Returns the width of a single marquee repetition of the edu text in pixels.
+ * This is the width from the start of the edu text to the start of the next edu
+ * text when it is marqueed.
+ *
+ * This is calculated based on the TextView.Marquee#start calculations
+ */
+ private float getMarqueeAnimEduTextLineWidth() {
+ // When the TextView has a marquee animation, it puts a gap between the text end and the
+ // start of the next edu text repetition. The space is equal to a third of the TextView
+ // width
+ final float gap = mEduTextView.getWidth() / 3.0f;
+ return getEduTextWidth() + gap;
+ }
+
+ private void startScrollEduText() {
+ ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d",
+ TAG, mEduTextView.getMarqueeRepeatLimit());
+ mEduTextView.setSelected(true);
+ }
+
+ /**
+ * Returns the width of the edu text irrespective of the TextView width
+ */
+ private int getEduTextWidth() {
+ return (int) mEduTextView.getLayout().getLineWidth(0);
+ }
+
+ /**
+ * Closes the edu text drawer if it hasn't been closed yet
+ */
+ void closeIfNeeded() {
+ if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) {
+ ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: close(), closing the edu text drawer because of user action", TAG);
+ mMainHandler.removeCallbacks(mCloseDrawerRunnable);
+ mCloseDrawerRunnable.run();
+ } else {
+ // Do nothing, the drawer has already been closed
+ }
+ }
+
+ private void closeDrawer() {
+ ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG);
+ final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger(
+ R.integer.pip_edu_text_view_exit_animation_duration);
+ final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger(
+ R.integer.pip_edu_text_window_exit_animation_duration);
+
+ // Start fading out the edu text
+ mEduTextView.animate()
+ .alpha(0f)
+ .setInterpolator(TvPipInterpolators.EXIT)
+ .setDuration(eduTextFadeExitAnimationDuration)
+ .start();
+
+ // Start animation to close the drawer by animating its height to 0
+ final ValueAnimator heightAnimation = ValueAnimator.ofInt(getHeight(), 0);
+ heightAnimation.setDuration(eduTextSlideExitAnimationDuration);
+ heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
+ heightAnimation.addUpdateListener(animator -> {
+ final ViewGroup.LayoutParams params = getLayoutParams();
+ params.height = (int) animator.getAnimatedValue();
+ setLayoutParams(params);
+ if (params.height == 0) {
+ setVisibility(GONE);
+ }
+ });
+ heightAnimation.start();
+
+ mListener.onCloseEduText();
+ }
+
+ /**
+ * Creates the educational text that will be displayed to the user. Here we replace the
+ * HOME annotation in the String with an icon
+ */
+ private CharSequence createEduTextString() {
+ final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
+ final SpannableString spannableString = new SpannableString(eduText);
+ Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
+ .ifPresent(annotation -> {
+ final Drawable icon =
+ getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
+ if (icon != null) {
+ icon.mutate();
+ icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+ spannableString.setSpan(new CenteredImageSpan(icon),
+ eduText.getSpanStart(annotation),
+ eduText.getSpanEnd(annotation),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ });
+
+ return spannableString;
+ }
+
+ /**
+ * A listener for edu text drawer event states.
+ */
+ interface Listener {
+ /**
+ * The edu text closing impacts the size of the Picture-in-Picture window and influences
+ * how it is positioned on the screen.
+ */
+ void onCloseEduText();
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index 9cd05b0eea75..57e95c416b3c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -25,18 +25,11 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
import static android.view.KeyEvent.KEYCODE_ENTER;
-import android.animation.ValueAnimator;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.Context;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
import android.os.Handler;
-import android.text.Annotation;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.SpannedString;
-import android.util.AttributeSet;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.SurfaceControl;
@@ -49,7 +42,6 @@ import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
-import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -61,7 +53,6 @@ import com.android.wm.shell.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
/**
@@ -74,21 +65,16 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
private static final int FIRST_CUSTOM_ACTION_POSITION = 3;
- @Nullable
- private Listener mListener;
+ private final Listener mListener;
private final LinearLayout mActionButtonsContainer;
private final View mMenuFrameView;
private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>();
private final View mPipFrameView;
private final View mPipView;
- private final TextView mEduTextView;
- private final View mEduTextContainerView;
+ private final TvPipMenuEduTextDrawer mEduTextDrawer;
private final int mPipMenuOuterSpace;
private final int mPipMenuBorderWidth;
- private final int mEduTextFadeExitAnimationDurationMs;
- private final int mEduTextSlideExitAnimationDurationMs;
- private int mEduTextHeight;
private final ImageView mArrowUp;
private final ImageView mArrowRight;
@@ -116,25 +102,17 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
private final int mResizeAnimationDuration;
private final AccessibilityManager mA11yManager;
+ private final Handler mMainHandler;
- public TvPipMenuView(@NonNull Context context) {
- this(context, null);
- }
-
- public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
+ public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler,
+ @NonNull Listener listener) {
+ super(context, null, 0, 0);
inflate(context, R.layout.tv_pip_menu, this);
+ mMainHandler = mainHandler;
+ mListener = listener;
+
mA11yManager = context.getSystemService(AccessibilityManager.class);
mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons);
@@ -166,9 +144,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left);
mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button);
- mEduTextView = findViewById(R.id.tv_pip_menu_edu_text);
- mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container);
-
mResizeAnimationDuration = context.getResources().getInteger(
R.integer.config_pipResizeAnimationDuration);
mPipMenuFadeAnimationDuration = context.getResources()
@@ -178,63 +153,18 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
.getDimensionPixelSize(R.dimen.pip_menu_outer_space);
mPipMenuBorderWidth = context.getResources()
.getDimensionPixelSize(R.dimen.pip_menu_border_width);
- mEduTextHeight = context.getResources()
- .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
- mEduTextFadeExitAnimationDurationMs = context.getResources()
- .getInteger(R.integer.pip_edu_text_view_exit_animation_duration_ms);
- mEduTextSlideExitAnimationDurationMs = context.getResources()
- .getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
-
- initEduText();
- }
- void initEduText() {
- final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
- final SpannableString spannableString = new SpannableString(eduText);
- Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
- .ifPresent(annotation -> {
- final Drawable icon =
- getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
- if (icon != null) {
- icon.mutate();
- icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
- spannableString.setSpan(new CenteredImageSpan(icon),
- eduText.getSpanStart(annotation),
- eduText.getSpanEnd(annotation),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- });
-
- mEduTextView.setText(spannableString);
+ mEduTextDrawer = new TvPipMenuEduTextDrawer(mContext, mainHandler, mListener);
+ ((FrameLayout) findViewById(R.id.tv_pip_menu_edu_text_drawer_placeholder))
+ .addView(mEduTextDrawer);
}
- void setEduTextActive(boolean active) {
- mEduTextView.setSelected(active);
- }
-
- void hideEduText() {
- final ValueAnimator heightAnimation = ValueAnimator.ofInt(mEduTextHeight, 0);
- heightAnimation.setDuration(mEduTextSlideExitAnimationDurationMs);
- heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
- heightAnimation.addUpdateListener(animator -> {
- mEduTextHeight = (int) animator.getAnimatedValue();
- });
- mEduTextView.animate()
- .alpha(0f)
- .setInterpolator(TvPipInterpolators.EXIT)
- .setDuration(mEduTextFadeExitAnimationDurationMs)
- .withEndAction(() -> {
- mEduTextContainerView.setVisibility(GONE);
- }).start();
- heightAnimation.start();
- }
-
- void onPipTransitionStarted(Rect finishBounds) {
+ void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
// Fade out content by fading in view on top.
- if (mCurrentPipBounds != null && finishBounds != null) {
+ if (mCurrentPipBounds != null && targetBounds != null) {
boolean ratioChanged = PipUtils.aspectRatioChanged(
mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(),
- finishBounds.width() / (float) finishBounds.height());
+ targetBounds.width() / (float) targetBounds.height());
if (ratioChanged) {
mPipBackground.animate()
.alpha(1f)
@@ -245,11 +175,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
}
// Update buttons.
- final boolean vertical = finishBounds.height() > finishBounds.width();
+ final boolean vertical = targetBounds.height() > targetBounds.width();
final boolean orientationChanged =
vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL);
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: onPipTransitionStarted(), orientation changed %b", TAG, orientationChanged);
+ "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b",
+ TAG, orientationChanged);
if (!orientationChanged) {
return;
}
@@ -261,18 +192,18 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
.setInterpolator(TvPipInterpolators.EXIT)
.setDuration(mResizeAnimationDuration / 2)
.withEndAction(() -> {
- changeButtonScrollOrientation(finishBounds);
- updateButtonGravity(finishBounds);
+ changeButtonScrollOrientation(targetBounds);
+ updateButtonGravity(targetBounds);
// Only make buttons visible again in onPipTransitionFinished to keep in
// sync with PiP content alpha animation.
});
} else {
- changeButtonScrollOrientation(finishBounds);
- updateButtonGravity(finishBounds);
+ changeButtonScrollOrientation(targetBounds);
+ updateButtonGravity(targetBounds);
}
}
- void onPipTransitionFinished(boolean isTvPipExpanded) {
+ void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransitionFinished()", TAG);
@@ -283,6 +214,10 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
.setInterpolator(TvPipInterpolators.ENTER)
.start();
+ if (enterTransition) {
+ mEduTextDrawer.init();
+ }
+
setIsExpanded(isTvPipExpanded);
// Update buttons.
@@ -409,7 +344,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
Rect getPipMenuContainerBounds(Rect pipBounds) {
final Rect menuUiBounds = new Rect(pipBounds);
menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
- menuUiBounds.bottom += mEduTextHeight;
+ menuUiBounds.bottom += mEduTextDrawer.getHeight();
return menuUiBounds;
}
@@ -438,10 +373,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
}
- void setListener(@Nullable Listener listener) {
- mListener = listener;
- }
-
void setExpandedModeEnabled(boolean enabled) {
mExpandButton.setVisibility(enabled ? VISIBLE : GONE);
}
@@ -460,21 +391,19 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
*/
void showMoveMenu(int gravity) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
- mButtonMenuIsVisible = false;
- mMoveMenuIsVisible = true;
showButtonsMenu(false);
showMovementHints(gravity);
setFrameHighlighted(true);
mHorizontalScrollView.setFocusable(false);
mScrollView.setFocusable(false);
+
+ mEduTextDrawer.closeIfNeeded();
}
void showButtonsMenu() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showButtonsMenu()", TAG);
- mButtonMenuIsVisible = true;
- mMoveMenuIsVisible = false;
showButtonsMenu(true);
hideMovementHints();
setFrameHighlighted(true);
@@ -501,8 +430,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: hideAllUserControls()", TAG);
mFocusedButton = null;
- mButtonMenuIsVisible = false;
- mMoveMenuIsVisible = false;
showButtonsMenu(false);
hideMovementHints();
setFrameHighlighted(false);
@@ -632,8 +559,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
@Override
public void onClick(View v) {
- if (mListener == null) return;
-
final int id = v.getId();
if (id == R.id.tv_pip_menu_fullscreen_button) {
mListener.onFullscreenButtonClick();
@@ -662,7 +587,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
- if (mListener != null && event.getAction() == ACTION_UP) {
+ if (event.getAction() == ACTION_UP) {
if (!mMoveMenuIsVisible) {
mFocusedButton = mActionButtonsContainer.getFocusedChild();
}
@@ -700,6 +625,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity));
+ if (mMoveMenuIsVisible) {
+ return;
+ }
+ mMoveMenuIsVisible = true;
+
animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp);
animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown);
animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft);
@@ -714,9 +644,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton);
if (a11yEnabled) {
mA11yDoneButton.setOnClickListener(v -> {
- if (mListener != null) {
- mListener.onExitMoveMode();
- }
+ mListener.onExitMoveMode();
});
}
}
@@ -725,9 +653,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
arrowView.setClickable(enabled);
if (enabled) {
arrowView.setOnClickListener(v -> {
- if (mListener != null) {
- mListener.onPipMovement(keycode);
- }
+ mListener.onPipMovement(keycode);
});
}
}
@@ -743,6 +669,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: hideMovementHints()", TAG);
+ if (!mMoveMenuIsVisible) {
+ return;
+ }
+ mMoveMenuIsVisible = false;
+
animateAlphaTo(0, mArrowUp);
animateAlphaTo(0, mArrowRight);
animateAlphaTo(0, mArrowDown);
@@ -756,19 +687,25 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
public void showButtonsMenu(boolean show) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: showUserActions: %b", TAG, show);
+ if (mButtonMenuIsVisible == show) {
+ return;
+ }
+ mButtonMenuIsVisible = show;
+
if (show) {
mActionButtonsContainer.setVisibility(VISIBLE);
refocusPreviousButton();
}
animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
animateAlphaTo(show ? 1 : 0, mDimLayer);
+ mEduTextDrawer.closeIfNeeded();
}
private void setFrameHighlighted(boolean highlighted) {
mMenuFrameView.setActivated(highlighted);
}
- interface Listener {
+ interface Listener extends TvPipMenuEduTextDrawer.Listener {
void onBackPress();
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
index 4f9ab6f00838..c8aa6d20c91b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
@@ -16,14 +16,12 @@
package com.android.wm.shell.flicker.pip
-import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerParametersRunnerFactory
import com.android.server.wm.flicker.FlickerTestParameter
import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
@@ -108,14 +106,6 @@ class EnterPipOnUserLeaveHintTest(testSpec: FlickerTestParameter) : EnterPipTest
@Presubmit
@Test
override fun entireScreenCovered() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- super.entireScreenCovered()
- }
-
- @FlakyTest(bugId = 227313015)
- @Test
- fun entireScreenCovered_ShellTransit() {
- Assume.assumeTrue(isShellTransitionsEnabled)
super.entireScreenCovered()
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
index 56e5e27e21ce..2b629e73e750 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt
@@ -16,7 +16,6 @@
package com.android.wm.shell.flicker.pip
-import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -83,8 +82,10 @@ open class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec
testSpec.assertWm { this.isAppWindowVisible(pipApp) }
}
- /** Checks [pipApp] layer remains visible throughout the animation */
- @FlakyTest(bugId = 239807171)
+ /**
+ * Checks [pipApp] layer remains visible throughout the animation
+ */
+ @Presubmit
@Test
open fun pipAppLayerAlwaysVisible() {
testSpec.assertLayers { this.isVisible(pipApp) }
@@ -104,7 +105,7 @@ open class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec
* Checks that the pip app layer remains inside the display bounds throughout the whole
* animation
*/
- @FlakyTest(bugId = 239807171)
+ @Presubmit
@Test
open fun pipLayerRemainInsideVisibleBounds() {
testSpec.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
index 11bc1f7ea406..104b409731dd 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
@@ -18,7 +18,6 @@ package com.android.wm.shell.flicker.pip
import android.app.Activity
import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
@@ -225,7 +224,7 @@ class EnterPipToOtherOrientationTest(
}
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
index 8cb68facab08..5e3194ca4ee2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
@@ -16,7 +16,7 @@
package com.android.wm.shell.flicker.pip
-import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
import android.view.Surface
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerParametersRunnerFactory
@@ -76,12 +76,12 @@ class ExitPipViaExpandButtonClickTest(
}
/** {@inheritDoc} */
- @FlakyTest(bugId = 227313015)
+ @Presubmit
@Test
override fun entireScreenCovered() = super.entireScreenCovered()
/** {@inheritDoc} */
- @FlakyTest(bugId = 197726610)
+ @Presubmit
@Test
override fun pipLayerExpands() = super.pipLayerExpands()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
index 18790713a828..3356d3e2ab2b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
@@ -74,8 +74,10 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit
}
}
- /** {@inheritDoc} */
- @FlakyTest @Test override fun entireScreenCovered() = super.entireScreenCovered()
+ /** {@inheritDoc} */
+ @Presubmit
+ @Test
+ override fun entireScreenCovered() = super.entireScreenCovered()
/** {@inheritDoc} */
@Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
new file mode 100644
index 000000000000..bcd01a414fd3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.wm.shell.flicker.pip
+
+import android.platform.test.annotations.Postsubmit
+import android.view.Surface
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test expanding a pip window via pinch out gesture.
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ExpandPipOnPinchOpenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = buildTransition {
+ transitions {
+ pipApp.pinchOpenPipWindow(wmHelper, 0.4f, 30)
+ }
+ }
+
+ /**
+ * Checks that the visible region area of [pipApp] always increases during the animation.
+ */
+ @Postsubmit
+ @Test
+ fun pipLayerAreaIncreases() {
+ testSpec.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ pipLayerList.zipWithNext { previous, current ->
+ previous.visibleRegion.notBiggerThan(current.visibleRegion.region)
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring
+ * repetitions, screen orientation and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTestParameter> {
+ return FlickerTestParameterFactory.getInstance()
+ .getConfigNonRotationTests(
+ supportedRotations = listOf(Surface.ROTATION_0)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
index 1d12154d9be5..7de5494a7733 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
@@ -161,7 +161,7 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS
testSpec.assertWmEnd { isAboveWindow(pipApp, testApp) }
}
- @FlakyTest(bugId = 240499181)
+ @Presubmit
@Test
override fun navBarLayerIsVisibleAtStartAndEnd() {
super.navBarLayerIsVisibleAtStartAndEnd()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 93be15484335..9b1247abfb71 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.flicker.splitscreen
import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.view.WindowManagerPolicyConstants
import androidx.test.filters.RequiresDevice
@@ -111,57 +110,60 @@ class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testS
@Presubmit @Test fun textEditAppWindowKeepVisible() = testSpec.appWindowKeepVisible(textEditApp)
/** {@inheritDoc} */
- @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+ @Presubmit
+ @Test
+ override fun entireScreenCovered() =
+ super.entireScreenCovered()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerIsVisibleAtStartAndEnd() =
super.statusBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
super.visibleWindowsShownMoreThanOneConsecutiveEntry()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
index 90e11f205ff1..cb49e18d672c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
@@ -18,7 +18,6 @@ package com.android.wm.shell.flicker.splitscreen
import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.view.WindowManagerPolicyConstants
import androidx.test.filters.RequiresDevice
@@ -107,67 +106,67 @@ class DismissSplitScreenByGoHome(
fun secondaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(secondaryApp)
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun entireScreenCovered() =
super.entireScreenCovered()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerIsVisibleAtStartAndEnd() =
super.navBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerPositionAtStartAndEnd() =
super.navBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarWindowIsAlwaysVisible() =
super.navBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerIsVisibleAtStartAndEnd() =
super.statusBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerPositionAtStartAndEnd() =
super.statusBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarWindowIsAlwaysVisible() =
super.statusBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarLayerIsVisibleAtStartAndEnd() =
super.taskBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarWindowIsAlwaysVisible() =
super.taskBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
super.visibleWindowsShownMoreThanOneConsecutiveEntry()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
index 6d821c491e82..504238f15b8c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
@@ -117,13 +117,13 @@ class EnterSplitScreenFromOverview(testSpec: FlickerTestParameter) : SplitScreen
fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun entireScreenCovered() =
super.entireScreenCovered()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerIsVisibleAtStartAndEnd() =
super.navBarLayerIsVisibleAtStartAndEnd()
@@ -135,49 +135,49 @@ class EnterSplitScreenFromOverview(testSpec: FlickerTestParameter) : SplitScreen
super.navBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarWindowIsAlwaysVisible() =
super.navBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerIsVisibleAtStartAndEnd() =
super.statusBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerPositionAtStartAndEnd() =
super.statusBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarWindowIsAlwaysVisible() =
super.statusBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarLayerIsVisibleAtStartAndEnd() =
super.taskBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarWindowIsAlwaysVisible() =
super.taskBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
super.visibleWindowsShownMoreThanOneConsecutiveEntry()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
index a340f6061ffb..2ecf81931e4a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.flicker.splitscreen
import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.view.WindowManagerPolicyConstants
import androidx.test.filters.RequiresDevice
@@ -105,57 +104,60 @@ class SwitchBackToSplitFromAnotherApp(testSpec: FlickerTestParameter) : SplitScr
fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
/** {@inheritDoc} */
- @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+ @Presubmit
+ @Test
+ override fun entireScreenCovered() =
+ super.entireScreenCovered()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerIsVisibleAtStartAndEnd() =
super.statusBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
super.visibleWindowsShownMoreThanOneConsecutiveEntry()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
index 8fdc9d6cb3bf..384489d99de3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.flicker.splitscreen
import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.view.WindowManagerPolicyConstants
import androidx.test.filters.RequiresDevice
@@ -104,57 +103,60 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas
fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
/** {@inheritDoc} */
- @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+ @Presubmit
+ @Test
+ override fun entireScreenCovered() =
+ super.entireScreenCovered()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerIsVisibleAtStartAndEnd() =
super.statusBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
super.visibleWindowsShownMoreThanOneConsecutiveEntry()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
index 5180389b9e7c..04ebbf527b3d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.flicker.splitscreen
import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.view.WindowManagerPolicyConstants
import androidx.test.filters.RequiresDevice
@@ -104,57 +103,60 @@ class SwitchBackToSplitFromRecent(testSpec: FlickerTestParameter) : SplitScreenB
fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
/** {@inheritDoc} */
- @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+ @Presubmit
+ @Test
+ override fun entireScreenCovered() =
+ super.entireScreenCovered()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerIsVisibleAtStartAndEnd() =
super.statusBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
/** {@inheritDoc} */
- @Postsubmit
+ @Presubmit
@Test
override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
super.visibleWindowsShownMoreThanOneConsecutiveEntry()
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index f50168250c96..1a76943102b1 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -44,8 +44,6 @@ public class SystemSettings {
Settings.System.DIM_SCREEN,
Settings.System.SCREEN_OFF_TIMEOUT,
Settings.System.SCREEN_BRIGHTNESS_MODE,
- Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
- Settings.System.SCREEN_BRIGHTNESS_FOR_VR,
Settings.System.ADAPTIVE_SLEEP, // moved to secure
Settings.System.APPLY_RAMPING_RINGER,
Settings.System.VIBRATE_INPUT_DEVICES,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index b1979c905510..8f6924ced582 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -100,7 +100,9 @@ public class SettingsBackupTest {
Settings.System.MIN_REFRESH_RATE, // depends on hardware capabilities
Settings.System.PEAK_REFRESH_RATE, // depends on hardware capabilities
Settings.System.SCREEN_BRIGHTNESS_FLOAT,
+ Settings.System.SCREEN_BRIGHTNESS_FOR_VR,
Settings.System.SCREEN_BRIGHTNESS_FOR_VR_FLOAT,
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
Settings.System.MULTI_AUDIO_FOCUS_ENABLED // form-factor/OEM specific
);
diff --git a/packages/SystemUI/res-keyguard/values/ids.xml b/packages/SystemUI/res-keyguard/values/ids.xml
new file mode 100644
index 000000000000..0dff4ffa3866
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ ~
+ -->
+
+<resources>
+ <item type="id" name="header_footer_views_added_tag_key" />
+</resources>
diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml
index d88680669fe0..ae8e38e2634b 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip.xml
@@ -16,7 +16,7 @@
<!-- Wrap in a frame layout so that we can update the margins on the inner layout. (Since this view
is the root view of a window, we cannot change the root view's margins.) -->
<!-- Alphas start as 0 because the view will be animated in. -->
-<FrameLayout
+<com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/media_ttt_sender_chip"
@@ -97,4 +97,4 @@
/>
</LinearLayout>
-</FrameLayout>
+</com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9324e8f3babb..637ac1911a85 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -246,6 +246,16 @@
<string name="screenrecord_start_label">Start Recording?</string>
<!-- Message reminding the user that sensitive information may be captured during a screen recording [CHAR_LIMIT=NONE]-->
<string name="screenrecord_description">While recording, Android System can capture any sensitive information that\u2019s visible on your screen or played on your device. This includes passwords, payment info, photos, messages, and audio.</string>
+ <!-- Dropdown option to record the entire screen [CHAR_LIMIT=30]-->
+ <string name="screenrecord_option_entire_screen">Record entire screen</string>
+ <!-- Dropdown option to record a single app [CHAR_LIMIT=30]-->
+ <string name="screenrecord_option_single_app">Record a single app</string>
+ <!-- Message reminding the user that sensitive information may be captured during a full screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]-->
+ <string name="screenrecord_warning_entire_screen">While you\'re recording, Android has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages, or other sensitive information.</string>
+ <!-- Message reminding the user that sensitive information may be captured during a single app screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]-->
+ <string name="screenrecord_warning_single_app">While you\'re recording an app, Android has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.</string>
+ <!-- Button to start a screen recording in the updated screen record dialog that allows to select an app to record [CHAR LIMIT=50]-->
+ <string name="screenrecord_start_recording">Start recording</string>
<!-- Label for a switch to enable recording audio [CHAR LIMIT=NONE]-->
<string name="screenrecord_audio_label">Record audio</string>
<!-- Label for the option to record audio from the device [CHAR LIMIT=NONE]-->
@@ -958,7 +968,26 @@
<!-- Media projection permission dialog warning title. [CHAR LIMIT=NONE] -->
<string name="media_projection_dialog_title">Start recording or casting with <xliff:g id="app_seeking_permission" example="Hangouts">%s</xliff:g>?</string>
- <!-- Media projection permission dialog permanent grant check box. [CHAR LIMIT=NONE] -->
+ <!-- Media projection permission dialog title. [CHAR LIMIT=NONE] -->
+ <string name="media_projection_permission_dialog_title">Allow <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> to share or record?</string>
+
+ <!-- Media projection permission dropdown option for capturing the whole screen. [CHAR LIMIT=30] -->
+ <string name="media_projection_permission_dialog_option_entire_screen">Entire screen</string>
+
+ <!-- Media projection permission dropdown option for capturing single app. [CHAR LIMIT=30] -->
+ <string name="media_projection_permission_dialog_option_single_app">A single app</string>
+
+ <!-- Media projection permission warning for capturing the whole screen. [CHAR LIMIT=350] -->
+ <string name="media_projection_permission_dialog_warning_entire_screen">When you\'re sharing, recording, or casting, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages, or other sensitive information.</string>
+
+ <!-- Media projection permission warning for capturing an app. [CHAR LIMIT=350] -->
+ <string name="media_projection_permission_dialog_warning_single_app">When you\'re sharing, recording, or casting an app, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.</string>
+
+ <!-- Media projection permission button to continue with app selection or recording [CHAR LIMIT=60] -->
+ <string name="media_projection_permission_dialog_continue">Continue</string>
+
+ <!-- Title of the dialog that allows to select an app to share or record [CHAR LIMIT=NONE] -->
+ <string name="media_projection_permission_app_selector_title">Share or record an app</string>
<!-- The text to clear all notifications. [CHAR LIMIT=60] -->
<string name="clear_all_notifications_text">Clear all</string>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index e77c65079456..2b2b05ce2fbf 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -81,11 +81,6 @@ interface ISystemUiProxy {
*/
void stopScreenPinning() = 17;
- /*
- * Notifies that the swipe-to-home (recents animation) is finished.
- */
- void notifySwipeToHomeFinished() = 23;
-
/**
* Notifies that quickstep will switch to a new task
* @param rotation indicates which Surface.Rotation the gesture was started in
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
index efa5558f5088..b793fd22aed1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
@@ -66,10 +66,13 @@ public class KeyguardUserSwitcherPopupMenu extends ListPopupWindow {
listView.setDividerHeight(mContext.getResources().getDimensionPixelSize(
R.dimen.bouncer_user_switcher_popup_divider_height));
- int height = mContext.getResources().getDimensionPixelSize(
- R.dimen.bouncer_user_switcher_popup_header_height);
- listView.addHeaderView(createSpacer(height), null, false);
- listView.addFooterView(createSpacer(height), null, false);
+ if (listView.getTag(R.id.header_footer_views_added_tag_key) == null) {
+ int height = mContext.getResources().getDimensionPixelSize(
+ R.dimen.bouncer_user_switcher_popup_header_height);
+ listView.addHeaderView(createSpacer(height), null, false);
+ listView.addFooterView(createSpacer(height), null, false);
+ listView.setTag(R.id.header_footer_views_added_tag_key, new Object());
+ }
listView.setOnTouchListener((v, ev) -> {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
new file mode 100644
index 000000000000..80b9c4e13c5c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -0,0 +1,53 @@
+package com.android.keyguard.logging
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.LogLevel.ERROR
+import com.android.systemui.log.LogLevel.VERBOSE
+import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.MessageInitializer
+import com.android.systemui.log.MessagePrinter
+import com.android.systemui.log.dagger.KeyguardLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "KeyguardLog"
+
+class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) {
+ fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+
+ fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
+
+ fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
+
+ fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
+
+ fun log(msg: String, level: LogLevel) = buffer.log(TAG, level, msg)
+
+ private fun debugLog(messageInitializer: MessageInitializer, messagePrinter: MessagePrinter) {
+ buffer.log(TAG, DEBUG, messageInitializer, messagePrinter)
+ }
+
+ // TODO: remove after b/237743330 is fixed
+ fun logStatusBarCalculatedAlpha(alpha: Float) {
+ debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" })
+ }
+
+ // TODO: remove after b/237743330 is fixed
+ fun logStatusBarExplicitAlpha(alpha: Float) {
+ debugLog({ double1 = alpha.toDouble() }, { "new mExplicitAlpha value: $double1" })
+ }
+
+ // TODO: remove after b/237743330 is fixed
+ fun logStatusBarAlphaVisibility(visibility: Int, alpha: Float, state: String) {
+ debugLog(
+ {
+ int1 = visibility
+ double1 = alpha.toDouble()
+ str1 = state
+ },
+ { "changing visibility to $int1 with alpha $double1 in state: $str1" }
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 7a00cd930f2a..bf9f4c88bde3 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -45,7 +45,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor(
fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
- fun v(@CompileTimeConstant msg: String) = log(msg, ERROR)
+ fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 443d2774f0e0..06dbab980793 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -81,6 +81,7 @@ import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule;
import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
import com.android.systemui.statusbar.window.StatusBarWindowModule;
+import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
import com.android.systemui.tuner.dagger.TunerModule;
import com.android.systemui.unfold.SysUIUnfoldModule;
import com.android.systemui.user.UserModule;
@@ -145,6 +146,7 @@ import dagger.Provides;
StatusBarWindowModule.class,
SysUIConcurrencyModule.class,
SysUIUnfoldModule.class,
+ TelephonyRepositoryModule.class,
TunerModule.class,
UserModule.class,
UtilModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 38d9d0210378..389a8c7700e2 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -68,6 +68,9 @@ public class Flags {
public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true);
+ public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112,
+ false);
+
// next id: 112
/***************************************/
@@ -104,9 +107,26 @@ public class Flags {
public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY =
new ReleasedFlag(209, true);
- /** Whether the new implementation of UserSwitcherController should be used. */
- public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER =
- new UnreleasedFlag(210, false);
+ /**
+ * Whether the user interactor and repository should use `UserSwitcherController`.
+ *
+ * <p>If this is {@code false}, the interactor and repo skip the controller and directly access
+ * the framework APIs.
+ */
+ public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
+ new UnreleasedFlag(210, true);
+
+ /**
+ * Whether `UserSwitcherController` should use the user interactor.
+ *
+ * <p>When this is {@code true}, the controller does not directly access framework APIs.
+ * Instead, it goes through the interactor.
+ *
+ * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is
+ * {@code true} as it would created a cycle between controller -> interactor -> controller.
+ */
+ public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR =
+ new UnreleasedFlag(211, false);
/***************************************/
// 300 - power menu
@@ -247,6 +267,13 @@ public class Flags {
public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES =
new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false);
+ @Keep
+ public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE =
+ new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true);
+ @Keep
+ public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP =
+ new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true);
+
// 1200 - predictive back
@Keep
public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 840a4b20a3f0..4c4b588888d1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -85,6 +85,15 @@ interface KeyguardRepository {
*/
val dozeAmount: Flow<Float>
+ /**
+ * Returns `true` if the keyguard is showing; `false` otherwise.
+ *
+ * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in
+ * the z-order (which is not really above the system UI window, but rather - the lock-screen
+ * becomes invisible to reveal the "occluding activity").
+ */
+ fun isKeyguardShowing(): Boolean
+
/** Sets whether the bottom area UI should animate the transition out of doze state. */
fun setAnimateDozingTransitions(animate: Boolean)
@@ -103,7 +112,7 @@ class KeyguardRepositoryImpl
@Inject
constructor(
statusBarStateController: StatusBarStateController,
- keyguardStateController: KeyguardStateController,
+ private val keyguardStateController: KeyguardStateController,
dozeHost: DozeHost,
) : KeyguardRepository {
private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
@@ -168,6 +177,10 @@ constructor(
awaitClose { statusBarStateController.removeCallback(callback) }
}
+ override fun isKeyguardShowing(): Boolean {
+ return keyguardStateController.isShowing
+ }
+
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.value = animate
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index dccc94178ed5..192919e32cf6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.Flow
class KeyguardInteractor
@Inject
constructor(
- repository: KeyguardRepository,
+ private val repository: KeyguardRepository,
) {
/**
* The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
@@ -40,4 +40,8 @@ constructor(
val isDozing: Flow<Boolean> = repository.isDozing
/** Whether the keyguard is showing ot not. */
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+
+ fun isKeyguardShowing(): Boolean {
+ return repository.isKeyguardShowing()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt
new file mode 100644
index 000000000000..aef3471ea8ad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt
@@ -0,0 +1,10 @@
+package com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/**
+ * A [com.android.systemui.log.LogBuffer] for keyguard-related stuff. Should be used mostly for
+ * adding temporary logs or logging from smaller classes when creating new separate log class might
+ * be an overkill.
+ */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class KeyguardLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index f2f6badde2cf..968dbe727c51 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -344,4 +344,14 @@ public class LogModule {
public static LogBuffer provideUdfpsLogBuffer(LogBufferFactory factory) {
return factory.create("UdfpsLog", 1000);
}
+
+ /**
+ * Provides a {@link LogBuffer} for general keyguard-related logs.
+ */
+ @Provides
+ @SysUISingleton
+ @KeyguardLog
+ public static LogBuffer provideKeyguardLogBuffer(LogBufferFactory factory) {
+ return factory.create("KeyguardLog", 250);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index 4379d25406bf..aae973dcc1c7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -25,6 +25,7 @@ import androidx.annotation.StringRes
import com.android.internal.logging.UiEventLogger
import com.android.internal.statusbar.IUndoMediaTransferCallback
import com.android.systemui.R
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS
/**
@@ -107,12 +108,15 @@ enum class ChipStateSender(
controllerSender: MediaTttChipControllerSender,
routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?,
- uiEventLogger: MediaTttSenderUiEventLogger
+ uiEventLogger: MediaTttSenderUiEventLogger,
+ falsingManager: FalsingManager,
): View.OnClickListener? {
if (undoCallback == null) {
return null
}
return View.OnClickListener {
+ if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+
uiEventLogger.logUndoClicked(
MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED
)
@@ -143,12 +147,15 @@ enum class ChipStateSender(
controllerSender: MediaTttChipControllerSender,
routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?,
- uiEventLogger: MediaTttSenderUiEventLogger
+ uiEventLogger: MediaTttSenderUiEventLogger,
+ falsingManager: FalsingManager,
): View.OnClickListener? {
if (undoCallback == null) {
return null
}
return View.OnClickListener {
+ if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+
uiEventLogger.logUndoClicked(
MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED
)
@@ -215,7 +222,8 @@ enum class ChipStateSender(
controllerSender: MediaTttChipControllerSender,
routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?,
- uiEventLogger: MediaTttSenderUiEventLogger
+ uiEventLogger: MediaTttSenderUiEventLogger,
+ falsingManager: FalsingManager,
): View.OnClickListener? = null
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index e539f3fd842d..007eb8f8deee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -22,25 +22,30 @@ import android.media.MediaRoute2Info
import android.os.PowerManager
import android.util.Log
import android.view.Gravity
+import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import android.widget.TextView
import com.android.internal.statusbar.IUndoMediaTransferCallback
+import com.android.systemui.Gefingerpoken
import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.media.taptotransfer.common.MediaTttUtils
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.temporarydisplay.TemporaryDisplayRemovalReason
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.android.systemui.util.concurrency.DelayableExecutor
+import dagger.Lazy
import javax.inject.Inject
/**
@@ -57,7 +62,11 @@ class MediaTttChipControllerSender @Inject constructor(
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
powerManager: PowerManager,
- private val uiEventLogger: MediaTttSenderUiEventLogger
+ private val uiEventLogger: MediaTttSenderUiEventLogger,
+ // Added Lazy<> to delay the time we create Falsing instances.
+ // And overcome performance issue, check [b/247817628] for details.
+ private val falsingManager: Lazy<FalsingManager>,
+ private val falsingCollector: Lazy<FalsingCollector>,
) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>(
context,
logger,
@@ -70,6 +79,9 @@ class MediaTttChipControllerSender @Inject constructor(
MediaTttUtils.WINDOW_TITLE,
MediaTttUtils.WAKE_REASON,
) {
+
+ private lateinit var parent: MediaTttChipRootView
+
override val windowLayoutParams = commonWindowLayoutParams.apply {
gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
}
@@ -120,6 +132,15 @@ class MediaTttChipControllerSender @Inject constructor(
val chipState = newInfo.state
+ // Detect falsing touches on the chip.
+ parent = currentView.requireViewById(R.id.media_ttt_sender_chip)
+ parent.touchHandler = object : Gefingerpoken {
+ override fun onTouchEvent(ev: MotionEvent?): Boolean {
+ falsingCollector.get().onTouchEvent(ev)
+ return false
+ }
+ }
+
// App icon
val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
context, newInfo.routeInfo.clientPackageName, logger
@@ -142,7 +163,11 @@ class MediaTttChipControllerSender @Inject constructor(
// Undo
val undoView = currentView.requireViewById<View>(R.id.undo)
val undoClickListener = chipState.undoClickListener(
- this, newInfo.routeInfo, newInfo.undoCallback, uiEventLogger
+ this,
+ newInfo.routeInfo,
+ newInfo.undoCallback,
+ uiEventLogger,
+ falsingManager.get(),
)
undoView.setOnClickListener(undoClickListener)
undoView.visibility = (undoClickListener != null).visibleIfTrue()
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
new file mode 100644
index 000000000000..3373159fba4e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.systemui.media.taptotransfer.sender
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.widget.FrameLayout
+import com.android.systemui.Gefingerpoken
+
+/** A simple subclass that allows for observing touch events on chip. */
+class MediaTttChipRootView(
+ context: Context,
+ attrs: AttributeSet?
+) : FrameLayout(context, attrs) {
+
+ /** Assign this field to observe touch events. */
+ var touchHandler: Gefingerpoken? = null
+
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+ touchHandler?.onTouchEvent(ev)
+ return super.dispatchTouchEvent(ev)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 0f1338e4e872..b26b42c802f4 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -57,7 +57,6 @@ import android.window.BackEvent;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.policy.GestureNavigationSettingsObserver;
-import com.android.internal.util.LatencyTracker;
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
@@ -199,7 +198,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
private final Rect mNavBarOverlayExcludedBounds = new Rect();
private final Region mExcludeRegion = new Region();
private final Region mUnrestrictedExcludeRegion = new Region();
- private final LatencyTracker mLatencyTracker;
+ private final Provider<NavigationBarEdgePanel> mNavBarEdgePanelProvider;
private final Provider<BackGestureTfClassifierProvider>
mBackGestureTfClassifierProviderProvider;
private final FeatureFlags mFeatureFlags;
@@ -339,7 +338,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
IWindowManager windowManagerService,
Optional<Pip> pipOptional,
FalsingManager falsingManager,
- LatencyTracker latencyTracker,
+ Provider<NavigationBarEdgePanel> navigationBarEdgePanelProvider,
Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider,
FeatureFlags featureFlags) {
super(broadcastDispatcher);
@@ -358,7 +357,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
mWindowManagerService = windowManagerService;
mPipOptional = pipOptional;
mFalsingManager = falsingManager;
- mLatencyTracker = latencyTracker;
+ mNavBarEdgePanelProvider = navigationBarEdgePanelProvider;
mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
mFeatureFlags = featureFlags;
mLastReportedConfig.setTo(mContext.getResources().getConfiguration());
@@ -583,8 +582,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
setEdgeBackPlugin(
mBackPanelControllerFactory.create(mContext));
} else {
- setEdgeBackPlugin(
- new NavigationBarEdgePanel(mContext, mLatencyTracker));
+ setEdgeBackPlugin(mNavBarEdgePanelProvider.get());
}
}
@@ -1091,7 +1089,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
private final IWindowManager mWindowManagerService;
private final Optional<Pip> mPipOptional;
private final FalsingManager mFalsingManager;
- private final LatencyTracker mLatencyTracker;
+ private final Provider<NavigationBarEdgePanel> mNavBarEdgePanelProvider;
private final Provider<BackGestureTfClassifierProvider>
mBackGestureTfClassifierProviderProvider;
private final FeatureFlags mFeatureFlags;
@@ -1111,7 +1109,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
IWindowManager windowManagerService,
Optional<Pip> pipOptional,
FalsingManager falsingManager,
- LatencyTracker latencyTracker,
+ Provider<NavigationBarEdgePanel> navBarEdgePanelProvider,
Provider<BackGestureTfClassifierProvider>
backGestureTfClassifierProviderProvider,
FeatureFlags featureFlags) {
@@ -1129,7 +1127,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
mWindowManagerService = windowManagerService;
mPipOptional = pipOptional;
mFalsingManager = falsingManager;
- mLatencyTracker = latencyTracker;
+ mNavBarEdgePanelProvider = navBarEdgePanelProvider;
mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
mFeatureFlags = featureFlags;
}
@@ -1152,7 +1150,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker
mWindowManagerService,
mPipOptional,
mFalsingManager,
- mLatencyTracker,
+ mNavBarEdgePanelProvider,
mBackGestureTfClassifierProviderProvider,
mFeatureFlags);
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
index 24efc762b39b..1230708d780a 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
@@ -52,9 +52,9 @@ import androidx.dynamicanimation.animation.SpringForce;
import com.android.internal.util.LatencyTracker;
import com.android.settingslib.Utils;
-import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.plugins.NavigationEdgeBackPlugin;
import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
import com.android.systemui.statusbar.VibratorHelper;
@@ -62,6 +62,8 @@ import com.android.systemui.statusbar.VibratorHelper;
import java.io.PrintWriter;
import java.util.concurrent.Executor;
+import javax.inject.Inject;
+
public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPlugin {
private static final String TAG = "NavigationBarEdgePanel";
@@ -282,11 +284,16 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl
};
private BackCallback mBackCallback;
- public NavigationBarEdgePanel(Context context, LatencyTracker latencyTracker) {
+ @Inject
+ public NavigationBarEdgePanel(
+ Context context,
+ LatencyTracker latencyTracker,
+ VibratorHelper vibratorHelper,
+ @Background Executor backgroundExecutor) {
super(context);
mWindowManager = context.getSystemService(WindowManager.class);
- mVibratorHelper = Dependency.get(VibratorHelper.class);
+ mVibratorHelper = vibratorHelper;
mDensity = context.getResources().getDisplayMetrics().density;
@@ -358,7 +365,6 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl
setVisibility(GONE);
- Executor backgroundExecutor = Dependency.get(Dependency.BACKGROUND_EXECUTOR);
boolean isPrimaryDisplay = mContext.getDisplayId() == DEFAULT_DISPLAY;
mRegionSamplingHelper = new RegionSamplingHelper(this,
new RegionSamplingHelper.SamplingCallback() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index b3e6f8ab3de6..7a44058a46c7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -60,6 +60,7 @@ import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
@@ -82,7 +83,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
private static final String EXTRA_VISIBLE = "visible";
private final Rect mQsBounds = new Rect();
- private final StatusBarStateController mStatusBarStateController;
+ private final SysuiStatusBarStateController mStatusBarStateController;
private final FalsingManager mFalsingManager;
private final KeyguardBypassController mBypassController;
private boolean mQsExpanded;
@@ -159,7 +160,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
* Progress of pull down from the center of the lock screen.
* @see com.android.systemui.statusbar.LockscreenShadeTransitionController
*/
- private float mFullShadeProgress;
+ private float mLockscreenToShadeProgress;
private boolean mOverScrolling;
@@ -177,7 +178,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
@Inject
public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
QSTileHost qsTileHost,
- StatusBarStateController statusBarStateController, CommandQueue commandQueue,
+ SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue,
@Named(QS_PANEL) MediaHost qsMediaHost,
@Named(QUICK_QS_PANEL) MediaHost qqsMediaHost,
KeyguardBypassController keyguardBypassController,
@@ -442,20 +443,19 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
}
private void updateQsState() {
- final boolean expanded = mQsExpanded || mInSplitShade;
- final boolean expandVisually = expanded || mStackScrollerOverscrolling
+ final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling
|| mHeaderAnimating;
- mQSPanelController.setExpanded(expanded);
+ mQSPanelController.setExpanded(mQsExpanded);
boolean keyguardShowing = isKeyguardState();
- mHeader.setVisibility((expanded || !keyguardShowing || mHeaderAnimating
+ mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating
|| mShowCollapsedOnKeyguard)
? View.VISIBLE
: View.INVISIBLE);
mHeader.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
- || (expanded && !mStackScrollerOverscrolling), mQuickQSPanelController);
+ || (mQsExpanded && !mStackScrollerOverscrolling), mQuickQSPanelController);
boolean qsPanelVisible = !mQsDisabled && expandVisually;
- boolean footerVisible = qsPanelVisible && (expanded || !keyguardShowing || mHeaderAnimating
- || mShowCollapsedOnKeyguard);
+ boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing
+ || mHeaderAnimating || mShowCollapsedOnKeyguard);
mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
if (mQSFooterActionController != null) {
mQSFooterActionController.setVisible(footerVisible);
@@ -463,7 +463,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
mQSFooterActionsViewModel.onVisibilityChangeRequested(footerVisible);
}
mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
- || (expanded && !mStackScrollerOverscrolling));
+ || (mQsExpanded && !mStackScrollerOverscrolling));
mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
if (DEBUG) {
Log.d(TAG, "Footer: " + footerVisible + ", QS Panel: " + qsPanelVisible);
@@ -586,7 +586,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
mTransitioningToFullShade = isTransitioningToFullShade;
updateShowCollapsedOnKeyguard();
}
- mFullShadeProgress = qsTransitionFraction;
+ mLockscreenToShadeProgress = qsTransitionFraction;
setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation,
isTransitioningToFullShade ? qsSquishinessFraction : mSquishinessFraction);
}
@@ -710,10 +710,13 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
}
if (mInSplitShade) {
// Large screens in landscape.
- if (mTransitioningToFullShade || isKeyguardState()) {
+ // Need to check upcoming state as for unlocked -> AOD transition current state is
+ // not updated yet, but we're transitioning and UI should already follow KEYGUARD state
+ if (mTransitioningToFullShade || mStatusBarStateController.getCurrentOrUpcomingState()
+ == StatusBarState.KEYGUARD) {
// Always use "mFullShadeProgress" on keyguard, because
// "panelExpansionFractions" is always 1 on keyguard split shade.
- return mFullShadeProgress;
+ return mLockscreenToShadeProgress;
} else {
return panelExpansionFraction;
}
@@ -722,7 +725,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
if (mTransitioningToFullShade) {
// Only use this value during the standard lock screen shade expansion. During the
// "quick" expansion from top, this value is 0.
- return mFullShadeProgress;
+ return mLockscreenToShadeProgress;
} else {
return panelExpansionFraction;
}
@@ -930,7 +933,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
indentingPw.println("mLastHeaderTranslation: " + mLastHeaderTranslation);
indentingPw.println("mInSplitShade: " + mInSplitShade);
indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade);
- indentingPw.println("mFullShadeProgress: " + mFullShadeProgress);
+ indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress);
indentingPw.println("mOverScrolling: " + mOverScrolling);
indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing());
View view = getView();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 97476b2d1cde..d2d5063c7ae0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -134,7 +134,7 @@ public class UserDetailView extends PseudoGridView {
v.bind(name, drawable, item.info.id);
}
v.setActivated(item.isCurrent);
- v.setDisabledByAdmin(mController.isDisabledByAdmin(item));
+ v.setDisabledByAdmin(item.isDisabledByAdmin());
v.setEnabled(item.isSwitchToEnabled);
UserSwitcherController.setSelectableAlpha(v);
@@ -173,16 +173,16 @@ public class UserDetailView extends PseudoGridView {
Trace.beginSection("UserDetailView.Adapter#onClick");
UserRecord userRecord =
(UserRecord) view.getTag();
- if (mController.isDisabledByAdmin(userRecord)) {
+ if (userRecord.isDisabledByAdmin()) {
final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
- mContext, mController.getEnforcedAdmin(userRecord));
+ mContext, userRecord.enforcedAdmin);
mController.startActivity(intent);
} else if (userRecord.isSwitchToEnabled) {
MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER);
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH);
if (!userRecord.isAddUser
&& !userRecord.isRestricted
- && !mController.isDisabledByAdmin(userRecord)) {
+ && !userRecord.isDisabledByAdmin()) {
if (mCurrentUserView != null) {
mCurrentUserView.setActivated(false);
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 7e2a5c51786d..899e57d7d0ae 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -342,14 +342,6 @@ public class OverviewProxyService extends CurrentUserTracker implements
}
@Override
- public void notifySwipeToHomeFinished() {
- verifyCallerAndClearCallingIdentity("notifySwipeToHomeFinished", () ->
- mPipOptional.ifPresent(
- pip -> pip.setPinnedStackAnimationType(
- PipAnimationController.ANIM_TYPE_ALPHA)));
- }
-
- @Override
public void notifySwipeUpGestureStarted() {
verifyCallerAndClearCallingIdentityPostMain("notifySwipeUpGestureStarted", () ->
notifySwipeUpGestureStartedInternal());
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
index 55602a98b8c5..e3658defc52a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
@@ -19,6 +19,7 @@ package com.android.systemui.screenshot;
import static android.os.FileUtils.closeQuietly;
import android.annotation.IntRange;
+import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.graphics.Bitmap;
@@ -29,6 +30,7 @@ import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.os.Trace;
+import android.os.UserHandle;
import android.provider.MediaStore;
import android.util.Log;
@@ -142,8 +144,9 @@ class ImageExporter {
*
* @return a listenable future result
*/
- ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap) {
- return export(executor, requestId, bitmap, ZonedDateTime.now());
+ ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
+ UserHandle owner) {
+ return export(executor, requestId, bitmap, ZonedDateTime.now(), owner);
}
/**
@@ -155,10 +158,10 @@ class ImageExporter {
* @return a listenable future result
*/
ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
- ZonedDateTime captureTime) {
+ ZonedDateTime captureTime, UserHandle owner) {
final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
- mQuality, /* publish */ true);
+ mQuality, /* publish */ true, owner);
return CallbackToFutureAdapter.getFuture(
(completer) -> {
@@ -174,28 +177,6 @@ class ImageExporter {
);
}
- /**
- * Delete the entry.
- *
- * @param executor the thread for execution
- * @param uri the uri of the image to publish
- *
- * @return a listenable future result
- */
- ListenableFuture<Result> delete(Executor executor, Uri uri) {
- return CallbackToFutureAdapter.getFuture((completer) -> {
- executor.execute(() -> {
- mResolver.delete(uri, null);
-
- Result result = new Result();
- result.uri = uri;
- result.deleted = true;
- completer.set(result);
- });
- return "ContentResolver#delete";
- });
- }
-
static class Result {
Uri uri;
UUID requestId;
@@ -203,7 +184,6 @@ class ImageExporter {
long timestamp;
CompressFormat format;
boolean published;
- boolean deleted;
@Override
public String toString() {
@@ -214,7 +194,6 @@ class ImageExporter {
sb.append(", timestamp=").append(timestamp);
sb.append(", format=").append(format);
sb.append(", published=").append(published);
- sb.append(", deleted=").append(deleted);
sb.append('}');
return sb.toString();
}
@@ -227,17 +206,19 @@ class ImageExporter {
private final ZonedDateTime mCaptureTime;
private final CompressFormat mFormat;
private final int mQuality;
+ private final UserHandle mOwner;
private final String mFileName;
private final boolean mPublish;
Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime,
- CompressFormat format, int quality, boolean publish) {
+ CompressFormat format, int quality, boolean publish, UserHandle owner) {
mResolver = resolver;
mRequestId = requestId;
mBitmap = bitmap;
mCaptureTime = captureTime;
mFormat = format;
mQuality = quality;
+ mOwner = owner;
mFileName = createFilename(mCaptureTime, mFormat);
mPublish = publish;
}
@@ -253,7 +234,7 @@ class ImageExporter {
start = Instant.now();
}
- uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName);
+ uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName, mOwner);
throwIfInterrupted();
writeImage(mResolver, mBitmap, mFormat, mQuality, uri);
@@ -297,15 +278,20 @@ class ImageExporter {
}
private static Uri createEntry(ContentResolver resolver, CompressFormat format,
- ZonedDateTime time, String fileName) throws ImageExportException {
+ ZonedDateTime time, String fileName, UserHandle owner) throws ImageExportException {
Trace.beginSection("ImageExporter_createEntry");
try {
final ContentValues values = createMetadata(time, format, fileName);
- Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+ Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ if (UserHandle.myUserId() != owner.getIdentifier()) {
+ baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier());
+ }
+ Uri uri = resolver.insert(baseUri, values);
if (uri == null) {
throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
}
+ Log.d(TAG, "Inserted new URI: " + uri);
return uri;
} finally {
Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index ba6e98e79ac0..8bf956b86683 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -30,6 +30,7 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
@@ -387,7 +388,9 @@ public class LongScreenshotActivity extends Activity {
mOutputBitmap = renderBitmap(drawable, bounds);
ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
- mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now());
+ mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now(),
+ // TODO: Owner must match the owner of the captured window.
+ Process.myUserHandle());
exportFuture.addListener(() -> onExportCompleted(action, exportFuture), mUiExecutor);
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index f248d6913878..077ad35fd63f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -48,6 +48,8 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.google.common.util.concurrent.ListenableFuture;
@@ -71,6 +73,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
private final Context mContext;
+ private FeatureFlags mFlags;
private final ScreenshotSmartActions mScreenshotSmartActions;
private final ScreenshotController.SaveImageInBackgroundData mParams;
private final ScreenshotController.SavedImageData mImageData;
@@ -84,7 +87,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
private final ImageExporter mImageExporter;
private long mImageTime;
- SaveImageInBackgroundTask(Context context, ImageExporter exporter,
+ SaveImageInBackgroundTask(
+ Context context,
+ FeatureFlags flags,
+ ImageExporter exporter,
ScreenshotSmartActions screenshotSmartActions,
ScreenshotController.SaveImageInBackgroundData data,
Supplier<ActionTransition> sharedElementTransition,
@@ -92,6 +98,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
screenshotNotificationSmartActionsProvider
) {
mContext = context;
+ mFlags = flags;
mScreenshotSmartActions = screenshotSmartActions;
mImageData = new ScreenshotController.SavedImageData();
mQuickShareData = new ScreenshotController.QuickShareData();
@@ -117,7 +124,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
}
// TODO: move to constructor / from ScreenshotRequest
final UUID requestId = UUID.randomUUID();
- final UserHandle user = getUserHandleOfForegroundApplication(mContext);
+ final UserHandle user = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)
+ ? mParams.owner : getUserHandleOfForegroundApplication(mContext);
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
@@ -133,8 +141,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
// Call synchronously here since already on a background thread.
ListenableFuture<ImageExporter.Result> future =
- mImageExporter.export(Runnable::run, requestId, image);
+ mImageExporter.export(Runnable::run, requestId, image, mParams.owner);
ImageExporter.Result result = future.get();
+ Log.d(TAG, "Saved screenshot: " + result);
final Uri uri = result.uri;
mImageTime = result.timestamp;
@@ -157,6 +166,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
}
mImageData.uri = uri;
+ mImageData.owner = user;
mImageData.smartActions = smartActions;
mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri);
mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 3fee232b3465..df32d2081fde 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -34,6 +34,7 @@ import static java.util.Objects.requireNonNull;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.MainThread;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -57,7 +58,9 @@ import android.media.AudioSystem;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Process;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -90,6 +93,7 @@ import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.clipboardoverlay.ClipboardOverlayController;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
import com.android.systemui.util.Assert;
@@ -151,6 +155,7 @@ public class ScreenshotController {
public Consumer<Uri> finisher;
public ScreenshotController.ActionsReadyListener mActionsReadyListener;
public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener;
+ public UserHandle owner;
void clearImage() {
image = null;
@@ -167,6 +172,8 @@ public class ScreenshotController {
public Notification.Action deleteAction;
public List<Notification.Action> smartActions;
public Notification.Action quickShareAction;
+ public UserHandle owner;
+
/**
* POD for shared element transition.
@@ -242,6 +249,7 @@ public class ScreenshotController {
private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
private final WindowContext mContext;
+ private final FeatureFlags mFlags;
private final ScreenshotNotificationsController mNotificationsController;
private final ScreenshotSmartActions mScreenshotSmartActions;
private final UiEventLogger mUiEventLogger;
@@ -288,6 +296,7 @@ public class ScreenshotController {
@Inject
ScreenshotController(
Context context,
+ FeatureFlags flags,
ScreenshotSmartActions screenshotSmartActions,
ScreenshotNotificationsController screenshotNotificationsController,
ScrollCaptureClient scrollCaptureClient,
@@ -331,6 +340,7 @@ public class ScreenshotController {
final Context displayContext = context.createDisplayContext(getDefaultDisplay());
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mWindowManager = mContext.getSystemService(WindowManager.class);
+ mFlags = flags;
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
@@ -377,7 +387,6 @@ public class ScreenshotController {
void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds,
Insets visibleInsets, int taskId, int userId, ComponentName topComponent,
Consumer<Uri> finisher, RequestCallback requestCallback) {
- // TODO: use task Id, userId, topComponent for smart handler
Assert.isMainThread();
if (screenshot == null) {
Log.e(TAG, "Got null bitmap from screenshot message");
@@ -395,7 +404,7 @@ public class ScreenshotController {
}
mCurrentRequestCallback = requestCallback;
saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent,
- showFlash);
+ showFlash, UserHandle.of(userId));
}
/**
@@ -543,14 +552,15 @@ public class ScreenshotController {
return;
}
- saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true);
+ saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true,
+ Process.myUserHandle());
mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION),
ClipboardOverlayController.SELF_PERMISSION);
}
private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
- Insets screenInsets, ComponentName topComponent, boolean showFlash) {
+ Insets screenInsets, ComponentName topComponent, boolean showFlash, UserHandle owner) {
withWindowAttached(() ->
mScreenshotView.announceForAccessibility(
mContext.getResources().getString(R.string.screenshot_saving_title)));
@@ -575,11 +585,11 @@ public class ScreenshotController {
mScreenBitmap = screenshot;
- if (!isUserSetupComplete()) {
+ if (!isUserSetupComplete(owner)) {
Log.w(TAG, "User setup not complete, displaying toast only");
// User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
// and sharing shouldn't be exposed to the user.
- saveScreenshotAndToast(finisher);
+ saveScreenshotAndToast(owner, finisher);
return;
}
@@ -587,7 +597,7 @@ public class ScreenshotController {
mScreenBitmap.setHasAlpha(false);
mScreenBitmap.prepareToDraw();
- saveScreenshotInWorkerThread(finisher, this::showUiOnActionsReady,
+ saveScreenshotInWorkerThread(owner, finisher, this::showUiOnActionsReady,
this::showUiOnQuickShareActionReady);
// The window is focusable by default
@@ -853,11 +863,12 @@ public class ScreenshotController {
* Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
* failure).
*/
- private void saveScreenshotAndToast(Consumer<Uri> finisher) {
+ private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) {
// Play the shutter sound to notify that we've taken a screenshot
playCameraSound();
saveScreenshotInWorkerThread(
+ owner,
/* onComplete */ finisher,
/* actionsReadyListener */ imageData -> {
if (DEBUG_CALLBACK) {
@@ -925,9 +936,11 @@ public class ScreenshotController {
/**
* Creates a new worker thread and saves the screenshot to the media store.
*/
- private void saveScreenshotInWorkerThread(Consumer<Uri> finisher,
- @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener,
- @Nullable ScreenshotController.QuickShareActionReadyListener
+ private void saveScreenshotInWorkerThread(
+ UserHandle owner,
+ @NonNull Consumer<Uri> finisher,
+ @Nullable ActionsReadyListener actionsReadyListener,
+ @Nullable QuickShareActionReadyListener
quickShareActionsReadyListener) {
ScreenshotController.SaveImageInBackgroundData
data = new ScreenshotController.SaveImageInBackgroundData();
@@ -935,13 +948,14 @@ public class ScreenshotController {
data.finisher = finisher;
data.mActionsReadyListener = actionsReadyListener;
data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
+ data.owner = owner;
if (mSaveInBgTask != null) {
// just log success/failure for the pre-existing screenshot
mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
}
- mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter,
+ mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter,
mScreenshotSmartActions, data, getActionTransitionSupplier(),
mScreenshotNotificationSmartActionsProvider);
mSaveInBgTask.execute();
@@ -960,6 +974,15 @@ public class ScreenshotController {
mScreenshotHandler.resetTimeout();
if (imageData.uri != null) {
+ if (!imageData.owner.equals(Process.myUserHandle())) {
+ // TODO: Handle non-primary user ownership (e.g. Work Profile)
+ // This image is owned by another user. Special treatment will be
+ // required in the UI (badging) as well as sending intents which can
+ // correctly forward those URIs on to be read (actions).
+
+ Log.d(TAG, "*** Screenshot saved to a non-primary user ("
+ + imageData.owner + ") as " + imageData.uri);
+ }
mScreenshotHandler.post(() -> {
if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
@@ -1033,9 +1056,9 @@ public class ScreenshotController {
}
}
- private boolean isUserSetupComplete() {
- return Settings.Secure.getInt(mContext.getContentResolver(),
- SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+ private boolean isUserSetupComplete(UserHandle owner) {
+ return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
+ .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
index c2a50609b6a5..3a3528606302 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
@@ -68,7 +68,9 @@ internal open class ScreenshotPolicyImpl @Inject constructor(
}
override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean {
- return withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
+ val managed = withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
+ Log.d(TAG, "isManagedProfile: $managed")
+ return managed
}
private fun nonPipVisibleTask(info: RootTaskInfo): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index 83b60fb23b90..30a0b8f2d76f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -78,6 +78,7 @@ public class ScrollCaptureController {
static class LongScreenshot {
private final ImageTileSet mImageTileSet;
private final Session mSession;
+ // TODO: Add UserHandle so LongScreenshots can adhere to work profile screenshot policy
LongScreenshot(Session session, ImageTileSet imageTileSet) {
mSession = session;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index d7e86b6e2919..f2d1b38fdb3a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -430,6 +430,7 @@ public final class NotificationPanelViewController extends PanelViewController {
/**
* Determines if QS should be already expanded when expanding shade.
* Used for split shade, two finger gesture as well as accessibility shortcut to QS.
+ * It needs to be set when movement starts as it resets at the end of expansion/collapse.
*/
@VisibleForTesting
boolean mQsExpandImmediate;
@@ -1737,8 +1738,10 @@ public final class NotificationPanelViewController extends PanelViewController {
}
private void setQsExpandImmediate(boolean expandImmediate) {
- mQsExpandImmediate = expandImmediate;
- mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate);
+ if (expandImmediate != mQsExpandImmediate) {
+ mQsExpandImmediate = expandImmediate;
+ mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate);
+ }
}
private void setShowShelfOnly(boolean shelfOnly) {
@@ -2479,17 +2482,23 @@ public final class NotificationPanelViewController extends PanelViewController {
mDepthController.setQsPanelExpansion(qsExpansionFraction);
mStatusBarKeyguardViewManager.setQsExpansion(qsExpansionFraction);
- // updateQsExpansion will get called whenever mTransitionToFullShadeProgress or
- // mLockscreenShadeTransitionController.getDragProgress change.
- // When in lockscreen, getDragProgress indicates the true expanded fraction of QS
- float shadeExpandedFraction = mTransitioningToFullShadeProgress > 0
- ? mLockscreenShadeTransitionController.getQSDragProgress()
+ float shadeExpandedFraction = isOnKeyguard()
+ ? getLockscreenShadeDragProgress()
: getExpandedFraction();
mLargeScreenShadeHeaderController.setShadeExpandedFraction(shadeExpandedFraction);
mLargeScreenShadeHeaderController.setQsExpandedFraction(qsExpansionFraction);
mLargeScreenShadeHeaderController.setQsVisible(mQsVisible);
}
+ private float getLockscreenShadeDragProgress() {
+ // mTransitioningToFullShadeProgress > 0 means we're doing regular lockscreen to shade
+ // transition. If that's not the case we should follow QS expansion fraction for when
+ // user is pulling from the same top to go directly to expanded QS
+ return mTransitioningToFullShadeProgress > 0
+ ? mLockscreenShadeTransitionController.getQSDragProgress()
+ : computeQsExpansionFraction();
+ }
+
private void onStackYChanged(boolean shouldAnimate) {
if (mQs != null) {
if (shouldAnimate) {
@@ -3124,26 +3133,24 @@ public final class NotificationPanelViewController extends PanelViewController {
}
if (mQsExpandImmediate || (mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
&& !mQsExpansionFromOverscroll)) {
- float t;
- if (mKeyguardShowing) {
-
+ float qsExpansionFraction;
+ if (mSplitShadeEnabled) {
+ qsExpansionFraction = 1;
+ } else if (mKeyguardShowing) {
// On Keyguard, interpolate the QS expansion linearly to the panel expansion
- t = expandedHeight / (getMaxPanelHeight());
+ qsExpansionFraction = expandedHeight / (getMaxPanelHeight());
} else {
// In Shade, interpolate linearly such that QS is closed whenever panel height is
// minimum QS expansion + minStackHeight
- float
- panelHeightQsCollapsed =
+ float panelHeightQsCollapsed =
mNotificationStackScrollLayoutController.getIntrinsicPadding()
+ mNotificationStackScrollLayoutController.getLayoutMinHeight();
float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
- t =
- (expandedHeight - panelHeightQsCollapsed) / (panelHeightQsExpanded
- - panelHeightQsCollapsed);
+ qsExpansionFraction = (expandedHeight - panelHeightQsCollapsed)
+ / (panelHeightQsExpanded - panelHeightQsCollapsed);
}
- float
- targetHeight =
- mQsMinExpansionHeight + t * (mQsMaxExpansionHeight - mQsMinExpansionHeight);
+ float targetHeight = mQsMinExpansionHeight
+ + qsExpansionFraction * (mQsMaxExpansionHeight - mQsMinExpansionHeight);
setQsExpansion(targetHeight);
}
updateExpandedHeight(expandedHeight);
@@ -3329,7 +3336,11 @@ public final class NotificationPanelViewController extends PanelViewController {
} else {
setListening(true);
}
- setQsExpandImmediate(false);
+ if (mBarState != SHADE) {
+ // updating qsExpandImmediate is done in onPanelStateChanged for unlocked shade but
+ // on keyguard panel state is always OPEN so we need to have that extra update
+ setQsExpandImmediate(false);
+ }
setShowShelfOnly(false);
mTwoFingerQsExpandPossible = false;
updateTrackingHeadsUp(null);
@@ -4678,12 +4689,19 @@ public final class NotificationPanelViewController extends PanelViewController {
}
}
} else {
+ // this else branch means we are doing one of:
+ // - from KEYGUARD and SHADE (but not expanded shade)
+ // - from SHADE to KEYGUARD
+ // - from SHADE_LOCKED to SHADE
+ // - getting notified again about the current SHADE or KEYGUARD state
final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE
&& statusBarState == KEYGUARD
&& mScreenOffAnimationController.isKeyguardShowDelayed();
if (!animatingUnlockedShadeToKeyguard) {
// Only make the status bar visible if we're not animating the screen off, since
// we only want to be showing the clock/notifications during the animation.
+ mShadeLog.v("Updating keyguard status bar state to "
+ + (keyguardShowing ? "visible" : "invisible"));
mKeyguardStatusBarViewController.updateViewState(
/* alpha= */ 1f,
keyguardShowing ? View.VISIBLE : View.INVISIBLE);
@@ -4749,9 +4767,7 @@ public final class NotificationPanelViewController extends PanelViewController {
@Override
public float getLockscreenShadeDragProgress() {
- return mTransitioningToFullShadeProgress > 0
- ? mLockscreenShadeTransitionController.getQSDragProgress()
- : computeQsExpansionFraction();
+ return NotificationPanelViewController.this.getLockscreenShadeDragProgress();
}
};
@@ -4988,6 +5004,7 @@ public final class NotificationPanelViewController extends PanelViewController {
updateQSExpansionEnabledAmbient();
if (state == STATE_OPEN && mCurrentPanelState != state) {
+ setQsExpandImmediate(false);
mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
if (state == STATE_OPENING) {
@@ -5000,6 +5017,7 @@ public final class NotificationPanelViewController extends PanelViewController {
mCentralSurfaces.makeExpandedVisible(false);
}
if (state == STATE_CLOSED) {
+ setQsExpandImmediate(false);
// Close the status bar in the next frame so we can show the end of the
// animation.
mView.post(mMaybeHideExpandedRunnable);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 8699441da726..c290ce260cc6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -42,7 +42,6 @@ import androidx.annotation.VisibleForTesting;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.internal.widget.LockPatternUtils;
-import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
@@ -97,6 +96,7 @@ public class NotificationLockscreenUserManagerImpl implements
private final List<UserChangedListener> mListeners = new ArrayList<>();
private final BroadcastDispatcher mBroadcastDispatcher;
private final NotificationClickNotifier mClickNotifier;
+ private final Lazy<OverviewProxyService> mOverviewProxyServiceLazy;
private boolean mShowLockscreenNotifications;
private boolean mAllowLockscreenRemoteInput;
@@ -157,7 +157,7 @@ public class NotificationLockscreenUserManagerImpl implements
break;
case Intent.ACTION_USER_UNLOCKED:
// Start the overview connection to the launcher service
- Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser();
+ mOverviewProxyServiceLazy.get().startConnectionToCurrentUser();
break;
case NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION:
final IntentSender intentSender = intent.getParcelableExtra(
@@ -199,6 +199,7 @@ public class NotificationLockscreenUserManagerImpl implements
Lazy<NotificationVisibilityProvider> visibilityProviderLazy,
Lazy<CommonNotifCollection> commonNotifCollectionLazy,
NotificationClickNotifier clickNotifier,
+ Lazy<OverviewProxyService> overviewProxyServiceLazy,
KeyguardManager keyguardManager,
StatusBarStateController statusBarStateController,
@Main Handler mainHandler,
@@ -214,6 +215,7 @@ public class NotificationLockscreenUserManagerImpl implements
mVisibilityProviderLazy = visibilityProviderLazy;
mCommonNotifCollectionLazy = commonNotifCollectionLazy;
mClickNotifier = clickNotifier;
+ mOverviewProxyServiceLazy = overviewProxyServiceLazy;
statusBarStateController.addCallback(this);
mLockPatternUtils = new LockPatternUtils(context);
mKeyguardManager = keyguardManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index c900c5a2ff0b..4be5a1aa0215 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -43,7 +43,6 @@ import android.util.Log;
import android.view.View;
import android.widget.ImageView;
-import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -89,11 +88,9 @@ public class NotificationMediaManager implements Dumpable {
private static final String TAG = "NotificationMediaManager";
public static final boolean DEBUG_MEDIA = false;
- private final StatusBarStateController mStatusBarStateController
- = Dependency.get(StatusBarStateController.class);
- private final SysuiColorExtractor mColorExtractor = Dependency.get(SysuiColorExtractor.class);
- private final KeyguardStateController mKeyguardStateController = Dependency.get(
- KeyguardStateController.class);
+ private final StatusBarStateController mStatusBarStateController;
+ private final SysuiColorExtractor mColorExtractor;
+ private final KeyguardStateController mKeyguardStateController;
private final KeyguardBypassController mKeyguardBypassController;
private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>();
private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>();
@@ -179,6 +176,9 @@ public class NotificationMediaManager implements Dumpable {
NotifCollection notifCollection,
@Main DelayableExecutor mainExecutor,
MediaDataManager mediaDataManager,
+ StatusBarStateController statusBarStateController,
+ SysuiColorExtractor colorExtractor,
+ KeyguardStateController keyguardStateController,
DumpManager dumpManager) {
mContext = context;
mMediaArtworkProcessor = mediaArtworkProcessor;
@@ -192,6 +192,9 @@ public class NotificationMediaManager implements Dumpable {
mMediaDataManager = mediaDataManager;
mNotifPipeline = notifPipeline;
mNotifCollection = notifCollection;
+ mStatusBarStateController = statusBarStateController;
+ mColorExtractor = colorExtractor;
+ mKeyguardStateController = keyguardStateController;
setupNotifPipeline();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 7cd79cac8928..11e3d1773c4c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -26,6 +26,7 @@ import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
@@ -130,6 +131,9 @@ public interface CentralSurfacesDependenciesModule {
NotifCollection notifCollection,
@Main DelayableExecutor mainExecutor,
MediaDataManager mediaDataManager,
+ StatusBarStateController statusBarStateController,
+ SysuiColorExtractor colorExtractor,
+ KeyguardStateController keyguardStateController,
DumpManager dumpManager) {
return new NotificationMediaManager(
context,
@@ -142,6 +146,9 @@ public interface CentralSurfacesDependenciesModule {
notifCollection,
mainExecutor,
mediaDataManager,
+ statusBarStateController,
+ colorExtractor,
+ keyguardStateController,
dumpManager);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 8278b549a7a0..ccf6feca6992 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -393,7 +393,7 @@ class HeadsUpCoordinator @Inject constructor(
val posted = mPostedEntries.compute(entry.key) { _, value ->
value?.also { update ->
update.wasUpdated = true
- update.shouldHeadsUpEver = update.shouldHeadsUpEver || shouldHeadsUpEver
+ update.shouldHeadsUpEver = shouldHeadsUpEver
update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain
update.isAlerting = isAlerting
update.isBinding = isBinding
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 801e544f03e6..8eef3f36433d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.init
import android.service.notification.StatusBarNotification
import com.android.systemui.ForegroundServiceNotificationListener
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.people.widget.PeopleSpaceWidgetManager
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
import com.android.systemui.statusbar.NotificationListener
@@ -38,6 +40,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor
import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -71,6 +74,8 @@ class NotificationsControllerImpl @Inject constructor(
private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
private val bubblesOptional: Optional<Bubbles>,
private val fgsNotifListener: ForegroundServiceNotificationListener,
+ private val memoryMonitor: Lazy<NotificationMemoryMonitor>,
+ private val featureFlags: FeatureFlags
) : NotificationsController {
override fun initialize(
@@ -112,6 +117,9 @@ class NotificationsControllerImpl @Inject constructor(
notificationLogger.setUpWithContainer(listContainer)
peopleSpaceWidgetManager.attach(notificationListener)
fgsNotifListener.init()
+ if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_MONITOR_ENABLED)) {
+ memoryMonitor.get().init()
+ }
}
// TODO: Convert all functions below this line into listeners instead of public methods
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
new file mode 100644
index 000000000000..832a739a9080
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
@@ -0,0 +1,41 @@
+/*
+ *
+ * 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.systemui.statusbar.notification.logging
+
+/** Describes usage of a notification. */
+data class NotificationMemoryUsage(
+ val packageName: String,
+ val notificationId: String,
+ val objectUsage: NotificationObjectUsage,
+)
+
+/**
+ * Describes current memory usage of a [android.app.Notification] object.
+ *
+ * The values are in bytes.
+ */
+data class NotificationObjectUsage(
+ val smallIcon: Int,
+ val largeIcon: Int,
+ val extras: Int,
+ val style: String?,
+ val styleIcon: Int,
+ val bigPicture: Int,
+ val extender: Int,
+ val hasCustomView: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
new file mode 100644
index 000000000000..ef7fa335c3cd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
@@ -0,0 +1,239 @@
+/*
+ *
+ * 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.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.Person
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.Log
+import androidx.annotation.WorkerThread
+import androidx.core.util.contains
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/** This class monitors and logs current Notification memory use. */
+@SysUISingleton
+class NotificationMemoryMonitor
+@Inject
+constructor(
+ val notificationPipeline: NotifPipeline,
+ val dumpManager: DumpManager,
+) : Dumpable {
+
+ companion object {
+ private const val TAG = "NotificationMemMonitor"
+ private const val CAR_EXTENSIONS = "android.car.EXTENSIONS"
+ private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon"
+ private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
+ private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
+ private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
+ }
+
+ fun init() {
+ Log.d(TAG, "NotificationMemoryMonitor initialized.")
+ dumpManager.registerDumpable(javaClass.simpleName, this)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) }
+ }
+
+ @WorkerThread
+ fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> {
+ return notificationMemoryUse(notificationPipeline.allNotifs)
+ }
+
+ /** Returns a list of memory use entries for currently shown notifications. */
+ @WorkerThread
+ fun notificationMemoryUse(
+ notifications: Collection<NotificationEntry>
+ ): List<NotificationMemoryUsage> {
+ return notifications.asSequence().map { entry ->
+ val packageName = entry.sbn.packageName
+ val notificationObjectUsage =
+ computeNotificationObjectUse(entry.sbn.notification, hashSetOf())
+ NotificationMemoryUsage(
+ packageName,
+ NotificationUtils.logKey(entry.sbn.key),
+ notificationObjectUsage)
+ }.toList()
+ }
+
+ /**
+ * Computes the estimated memory usage of a given [Notification] object. It'll attempt to
+ * inspect Bitmaps in the object and provide summary of memory usage.
+ */
+ private fun computeNotificationObjectUse(
+ notification: Notification,
+ seenBitmaps: HashSet<Int>
+ ): NotificationObjectUsage {
+ val extras = notification.extras
+ val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps)
+ val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps)
+
+ // Collect memory usage of extra styles
+
+ // Big Picture
+ val bigPictureIconUse =
+ computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) +
+ computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps)
+ val bigPictureUse =
+ computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) +
+ computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps)
+
+ // People
+ val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST)
+ val peopleUse =
+ peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0
+
+ // Calling
+ val callingPersonUse =
+ computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps)
+ val verificationIconUse =
+ computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps)
+
+ // Messages
+ val messages =
+ Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+ extras.getParcelableArray(Notification.EXTRA_MESSAGES)
+ )
+ val messagesUse =
+ messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+ val historicMessages =
+ Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+ extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
+ )
+ val historyicMessagesUse =
+ historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+
+ // Extenders
+ val carExtender = extras.getBundle(CAR_EXTENSIONS)
+ val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0
+ val carExtenderIcon =
+ computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps)
+
+ val tvExtender = extras.getBundle(TV_EXTENSIONS)
+ val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0
+
+ val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS)
+ val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0
+ val wearExtenderBackground =
+ computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
+
+ val style = notification.notificationStyle
+ val hasCustomView = notification.contentView != null || notification.bigContentView != null
+ val extrasSize = computeBundleSize(extras)
+
+ return NotificationObjectUsage(
+ smallIconUse,
+ largeIconUse,
+ extrasSize,
+ style?.simpleName,
+ bigPictureIconUse +
+ peopleUse +
+ callingPersonUse +
+ verificationIconUse +
+ messagesUse +
+ historyicMessagesUse,
+ bigPictureUse,
+ carExtenderSize +
+ carExtenderIcon +
+ tvExtenderSize +
+ wearExtenderSize +
+ wearExtenderBackground,
+ hasCustomView
+ )
+ }
+
+ /**
+ * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
+ * bitmaps). Can be slow.
+ */
+ private fun computeBundleSize(extras: Bundle): Int {
+ val parcel = Parcel.obtain()
+ try {
+ extras.writeToParcel(parcel, 0)
+ return parcel.dataSize()
+ } finally {
+ parcel.recycle()
+ }
+ }
+
+ /**
+ * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0
+ * if the key does not exist in extras.
+ */
+ private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int {
+ return when (val parcelable = extras?.getParcelable<Parcelable>(key)) {
+ is Bitmap -> computeBitmapUse(parcelable, seenBitmaps)
+ is Icon -> computeIconUse(parcelable, seenBitmaps)
+ is Person -> computeIconUse(parcelable.icon, seenBitmaps)
+ else -> 0
+ }
+ }
+
+ /**
+ * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is
+ * defined via Uri or a resource.
+ *
+ * @return memory usage in bytes or 0 if the icon is Uri/Resource based
+ */
+ private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) =
+ when (icon?.type) {
+ Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+ Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+ Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps)
+ else -> 0
+ }
+
+ /**
+ * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of
+ * seenBitmaps set, this method returns 0 to avoid double counting.
+ *
+ * @return memory usage of the bitmap in bytes
+ */
+ private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int {
+ val refId = System.identityHashCode(bitmap)
+ if (seenBitmaps?.contains(refId) == true) {
+ return 0
+ }
+
+ seenBitmaps?.add(refId)
+ return bitmap.allocationByteCount
+ }
+
+ private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int {
+ val refId = System.identityHashCode(icon.dataBytes)
+ if (seenBitmaps.contains(refId)) {
+ return 0
+ }
+
+ seenBitmaps.add(refId)
+ return icon.dataLength
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
index 9faef1b43bc1..5ca13c95309f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
@@ -45,11 +45,21 @@ public interface NotificationPanelLogger {
void logPanelShown(boolean isLockscreen,
@Nullable List<NotificationEntry> visibleNotifications);
+ /**
+ * Log a NOTIFICATION_PANEL_REPORTED statsd event, with
+ * {@link NotificationPanelEvent#NOTIFICATION_DRAG} as the eventID.
+ *
+ * @param draggedNotification the notification that is being dragged
+ */
+ void logNotificationDrag(NotificationEntry draggedNotification);
+
enum NotificationPanelEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "Notification panel shown from status bar.")
NOTIFICATION_PANEL_OPEN_STATUS_BAR(200),
@UiEvent(doc = "Notification panel shown from lockscreen.")
- NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201);
+ NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201),
+ @UiEvent(doc = "Notification was dragged")
+ NOTIFICATION_DRAG(1226);
private final int mId;
NotificationPanelEvent(int id) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
index 75a60194f2fa..9a632282ae16 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
@@ -16,12 +16,15 @@
package com.android.systemui.statusbar.notification.logging;
+import static com.android.systemui.statusbar.notification.logging.NotificationPanelLogger.NotificationPanelEvent.NOTIFICATION_DRAG;
+
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.logging.nano.Notifications;
import com.google.protobuf.nano.MessageNano;
+import java.util.Collections;
import java.util.List;
/**
@@ -38,4 +41,14 @@ public class NotificationPanelLoggerImpl implements NotificationPanelLogger {
/* int num_notifications*/ proto.notifications.length,
/* byte[] notifications*/ MessageNano.toByteArray(proto));
}
+
+ @Override
+ public void logNotificationDrag(NotificationEntry draggedNotification) {
+ final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto(
+ Collections.singletonList(draggedNotification));
+ SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED,
+ /* int event_id */ NOTIFICATION_DRAG.getId(),
+ /* int num_notifications*/ proto.notifications.length,
+ /* byte[] notifications*/ MessageNano.toByteArray(proto));
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
index 4939a9c22cf8..64f87cabaf74 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
@@ -45,12 +45,17 @@ import android.widget.Toast;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
import com.android.systemui.statusbar.policy.HeadsUpManager;
+import java.util.Collections;
+
import javax.inject.Inject;
/**
@@ -63,14 +68,17 @@ public class ExpandableNotificationRowDragController {
private final Context mContext;
private final HeadsUpManager mHeadsUpManager;
private final ShadeController mShadeController;
+ private NotificationPanelLogger mNotificationPanelLogger;
@Inject
public ExpandableNotificationRowDragController(Context context,
HeadsUpManager headsUpManager,
- ShadeController shadeController) {
+ ShadeController shadeController,
+ NotificationPanelLogger notificationPanelLogger) {
mContext = context;
mHeadsUpManager = headsUpManager;
mShadeController = shadeController;
+ mNotificationPanelLogger = notificationPanelLogger;
init();
}
@@ -120,12 +128,16 @@ public class ExpandableNotificationRowDragController {
dragIntent.putExtra(ClipDescription.EXTRA_PENDING_INTENT, contentIntent);
dragIntent.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle());
ClipData.Item item = new ClipData.Item(dragIntent);
+ InstanceId instanceId = new InstanceIdSequence(Integer.MAX_VALUE).newInstanceId();
+ item.getIntent().putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, instanceId);
ClipData dragData = new ClipData(clipDescription, item);
View.DragShadowBuilder myShadow = new View.DragShadowBuilder(snapshot);
view.setOnDragListener(getDraggedViewDragListener());
boolean result = view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL
| View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
if (result) {
+ // Log notification drag only if it succeeds
+ mNotificationPanelLogger.logNotificationDrag(enr.getEntry());
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if (enr.isPinned()) {
mHeadsUpManager.releaseAllImmediately();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 0026b71a5304..054bd28e003d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -40,6 +40,7 @@ import androidx.annotation.VisibleForTesting;
import com.android.keyguard.CarrierTextController;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.logging.KeyguardLogger;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.battery.BatteryMeterViewController;
@@ -116,6 +117,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
private final CommandQueue mCommandQueue;
private final Executor mMainExecutor;
private final Object mLock = new Object();
+ private final KeyguardLogger mLogger;
private final ConfigurationController.ConfigurationListener mConfigurationListener =
new ConfigurationController.ConfigurationListener() {
@@ -279,7 +281,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
StatusBarUserInfoTracker statusBarUserInfoTracker,
SecureSettings secureSettings,
CommandQueue commandQueue,
- @Main Executor mainExecutor
+ @Main Executor mainExecutor,
+ KeyguardLogger logger
) {
super(view);
mCarrierTextController = carrierTextController;
@@ -304,6 +307,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
mSecureSettings = secureSettings;
mCommandQueue = commandQueue;
mMainExecutor = mainExecutor;
+ mLogger = logger;
mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
mKeyguardStateController.addCallback(
@@ -430,6 +434,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
/** Animate the keyguard status bar in. */
public void animateKeyguardStatusBarIn() {
+ mLogger.d("animating status bar in");
if (mDisableStateTracker.isDisabled()) {
// If our view is disabled, don't allow us to animate in.
return;
@@ -445,6 +450,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
/** Animate the keyguard status bar out. */
public void animateKeyguardStatusBarOut(long startDelay, long duration) {
+ mLogger.d("animating status bar out");
ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f);
anim.addUpdateListener(mAnimatorUpdateListener);
anim.setStartDelay(startDelay);
@@ -481,6 +487,9 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
newAlpha = Math.min(getKeyguardContentsAlpha(), alphaQsExpansion)
* mKeyguardStatusBarAnimateAlpha
* (1.0f - mKeyguardHeadsUpShowingAmount);
+ if (newAlpha != mView.getAlpha() && (newAlpha == 0 || newAlpha == 1)) {
+ mLogger.logStatusBarCalculatedAlpha(newAlpha);
+ }
}
boolean hideForBypass =
@@ -503,6 +512,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
if (mDisableStateTracker.isDisabled()) {
visibility = View.INVISIBLE;
}
+ if (visibility != mView.getVisibility()) {
+ mLogger.logStatusBarAlphaVisibility(visibility, alpha,
+ StatusBarState.toString(mStatusBarState));
+ }
mView.setAlpha(alpha);
mView.setVisibility(visibility);
}
@@ -596,6 +609,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
pw.println("KeyguardStatusBarView:");
pw.println(" mBatteryListening: " + mBatteryListening);
pw.println(" mExplicitAlpha: " + mExplicitAlpha);
+ pw.println(" alpha: " + mView.getAlpha());
+ pw.println(" visibility: " + mView.getVisibility());
mView.dump(pw, args);
}
@@ -605,6 +620,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
* @param alpha a value between 0 and 1. -1 if the value is to be reset/ignored.
*/
public void setAlpha(float alpha) {
+ if (mExplicitAlpha != alpha && (mExplicitAlpha == -1 || alpha == -1)) {
+ // logged if value changed to ignored or from ignored
+ mLogger.logStatusBarExplicitAlpha(alpha);
+ }
mExplicitAlpha = alpha;
updateViewState();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index 0995a00533a8..712953e14d60 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -505,7 +505,7 @@ public class KeyguardUserSwitcherController extends ViewController<KeyguardUserS
v.bind(name, drawable, item.info.id);
}
v.setActivated(item.isCurrent);
- v.setDisabledByAdmin(getController().isDisabledByAdmin(item));
+ v.setDisabledByAdmin(item.isDisabledByAdmin());
v.setEnabled(item.isSwitchToEnabled);
UserSwitcherController.setSelectableAlpha(v);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
index 843c2329092c..146b222c94ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy
import android.annotation.UserIdInt
import android.content.Intent
import android.view.View
-import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
import com.android.systemui.Dumpable
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
import com.android.systemui.user.data.source.UserRecord
@@ -130,12 +129,6 @@ interface UserSwitcherController : Dumpable {
/** Whether keyguard is showing. */
val isKeyguardShowing: Boolean
- /** Returns the [EnforcedAdmin] for the given record, or `null` if there isn't one. */
- fun getEnforcedAdmin(record: UserRecord): EnforcedAdmin?
-
- /** Returns `true` if the given record is disabled by the admin; `false` otherwise. */
- fun isDisabledByAdmin(record: UserRecord): Boolean
-
/** Starts an activity with the given [Intent]. */
fun startActivity(intent: Intent)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
index 12834f68c3b7..16926566105c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
@@ -17,13 +17,21 @@
package com.android.systemui.statusbar.policy
+import android.content.Context
import android.content.Intent
import android.view.View
-import com.android.settingslib.RestrictedLockUtils
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
+import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import dagger.Lazy
import java.io.PrintWriter
import java.lang.ref.WeakReference
@@ -31,58 +39,76 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
/** Implementation of [UserSwitcherController]. */
+@SysUISingleton
class UserSwitcherControllerImpl
@Inject
constructor(
- private val flags: FeatureFlags,
+ @Application private val applicationContext: Context,
+ flags: FeatureFlags,
@Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>,
+ private val userInteractorLazy: Lazy<UserInteractor>,
+ private val guestUserInteractorLazy: Lazy<GuestUserInteractor>,
+ private val keyguardInteractorLazy: Lazy<KeyguardInteractor>,
+ private val activityStarter: ActivityStarter,
) : UserSwitcherController {
- private val isNewImpl: Boolean
- get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER)
+ private val useInteractor: Boolean =
+ flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) &&
+ !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
private val _oldImpl: UserSwitcherControllerOldImpl
get() = oldImpl.get()
+ private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() }
+ private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() }
+ private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() }
- private fun notYetImplemented(): Nothing {
- error("Not yet implemented!")
+ private val callbackCompatMap =
+ mutableMapOf<UserSwitcherController.UserSwitchCallback, UserInteractor.UserCallback>()
+
+ private fun notSupported(): Nothing {
+ error("Not supported in the new implementation!")
}
override val users: ArrayList<UserRecord>
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.userRecords.value
} else {
_oldImpl.users
}
override val isSimpleUserSwitcher: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.isSimpleUserSwitcher
} else {
_oldImpl.isSimpleUserSwitcher
}
override fun init(view: View) {
- if (isNewImpl) {
- notYetImplemented()
- } else {
+ if (!useInteractor) {
_oldImpl.init(view)
}
}
override val currentUserRecord: UserRecord?
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.selectedUserRecord.value
} else {
_oldImpl.currentUserRecord
}
override val currentUserName: String?
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ currentUserRecord?.let {
+ LegacyUserUiHelper.getUserRecordName(
+ context = applicationContext,
+ record = it,
+ isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
+ isGuestUserResetting = userInteractor.isGuestUserResetting,
+ )
+ }
} else {
_oldImpl.currentUserName
}
@@ -91,8 +117,8 @@ constructor(
userId: Int,
dialogShower: UserSwitchDialogController.DialogShower?
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.selectUser(userId)
} else {
_oldImpl.onUserSelected(userId, dialogShower)
}
@@ -100,24 +126,24 @@ constructor(
override val isAddUsersFromLockScreenEnabled: Flow<Boolean>
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.isAddUsersFromLockScreenEnabled
}
override val isGuestUserAutoCreated: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.isGuestUserAutoCreated
} else {
_oldImpl.isGuestUserAutoCreated
}
override val isGuestUserResetting: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.isGuestUserResetting
} else {
_oldImpl.isGuestUserResetting
}
@@ -125,40 +151,48 @@ constructor(
override fun createAndSwitchToGuestUser(
dialogShower: UserSwitchDialogController.DialogShower?,
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.createAndSwitchToGuestUser(dialogShower)
}
}
override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.showAddUserDialog(dialogShower)
}
}
override fun startSupervisedUserActivity() {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.startSupervisedUserActivity()
}
}
override fun onDensityOrFontScaleChanged() {
- if (isNewImpl) {
- notYetImplemented()
- } else {
+ if (!useInteractor) {
_oldImpl.onDensityOrFontScaleChanged()
}
}
override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.addCallback(
+ object : UserInteractor.UserCallback {
+ override fun isEvictable(): Boolean {
+ return adapter.get() == null
+ }
+
+ override fun onUserStateChanged() {
+ adapter.get()?.notifyDataSetChanged()
+ }
+ }
+ )
} else {
_oldImpl.addAdapter(adapter)
}
@@ -168,16 +202,23 @@ constructor(
record: UserRecord,
dialogShower: UserSwitchDialogController.DialogShower?,
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ if (LegacyUserDataHelper.isUser(record)) {
+ userInteractor.selectUser(record.resolveId())
+ } else {
+ userInteractor.executeAction(LegacyUserDataHelper.toUserActionModel(record))
+ }
} else {
_oldImpl.onUserListItemClicked(record, dialogShower)
}
}
override fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.removeGuestUser(
+ guestUserId = guestUserId,
+ targetUserId = targetUserId,
+ )
} else {
_oldImpl.removeGuestUser(guestUserId, targetUserId)
}
@@ -188,16 +229,16 @@ constructor(
targetUserId: Int,
forceRemoveGuestOnExit: Boolean
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
} else {
_oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
}
}
override fun schedulePostBootGuestCreation() {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ guestUserInteractor.onDeviceBootCompleted()
} else {
_oldImpl.schedulePostBootGuestCreation()
}
@@ -205,63 +246,57 @@ constructor(
override val isKeyguardShowing: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ keyguardInteractor.isKeyguardShowing()
} else {
_oldImpl.isKeyguardShowing
}
- override fun getEnforcedAdmin(record: UserRecord): RestrictedLockUtils.EnforcedAdmin? {
- return if (isNewImpl) {
- notYetImplemented()
- } else {
- _oldImpl.getEnforcedAdmin(record)
- }
- }
-
- override fun isDisabledByAdmin(record: UserRecord): Boolean {
- return if (isNewImpl) {
- notYetImplemented()
- } else {
- _oldImpl.isDisabledByAdmin(record)
- }
- }
-
override fun startActivity(intent: Intent) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ activityStarter.startActivity(intent, /* dismissShade= */ false)
} else {
_oldImpl.startActivity(intent)
}
}
override fun refreshUsers(forcePictureLoadForId: Int) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.refreshUsers()
} else {
_oldImpl.refreshUsers(forcePictureLoadForId)
}
}
override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ val interactorCallback =
+ object : UserInteractor.UserCallback {
+ override fun onUserStateChanged() {
+ callback.onUserSwitched()
+ }
+ }
+ callbackCompatMap[callback] = interactorCallback
+ userInteractor.addCallback(interactorCallback)
} else {
_oldImpl.addUserSwitchCallback(callback)
}
}
override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ val interactorCallback = callbackCompatMap.remove(callback)
+ if (interactorCallback != null) {
+ userInteractor.removeCallback(interactorCallback)
+ }
} else {
_oldImpl.removeUserSwitchCallback(callback)
}
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.dump(pw)
} else {
_oldImpl.dump(pw, args)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
index d365aa6f952d..46d2f3ac9ce4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
@@ -17,17 +17,13 @@ package com.android.systemui.statusbar.policy;
import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
-import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
-
import android.annotation.UserIdInt;
-import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.IActivityManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
@@ -40,7 +36,6 @@ import android.os.UserManager;
import android.provider.Settings;
import android.telephony.TelephonyCallback;
import android.text.TextUtils;
-import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -49,17 +44,14 @@ import android.view.WindowManagerGlobal;
import android.widget.Toast;
import androidx.annotation.Nullable;
-import androidx.collection.SimpleArrayMap;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.LatencyTracker;
-import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.users.UserCreatingDialog;
import com.android.systemui.GuestResetOrExitSessionReceiver;
import com.android.systemui.GuestResumeSessionReceiver;
-import com.android.systemui.R;
import com.android.systemui.SystemUISecondaryUserService;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogLaunchAnimator;
@@ -75,10 +67,12 @@ import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.qs.QSUserSwitcherEvent;
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower;
import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.telephony.TelephonyListenerManager;
-import com.android.systemui.user.CreateUserActivity;
import com.android.systemui.user.data.source.UserRecord;
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper;
+import com.android.systemui.user.shared.model.UserActionModel;
+import com.android.systemui.user.ui.dialog.AddUserDialog;
+import com.android.systemui.user.ui.dialog.ExitGuestDialog;
import com.android.systemui.util.settings.GlobalSettings;
import com.android.systemui.util.settings.SecureSettings;
@@ -139,9 +133,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
private final InteractionJankMonitor mInteractionJankMonitor;
private final LatencyTracker mLatencyTracker;
private final DialogLaunchAnimator mDialogLaunchAnimator;
- private final SimpleArrayMap<UserRecord, EnforcedAdmin> mEnforcedAdminByUserRecord =
- new SimpleArrayMap<>();
- private final ArraySet<UserRecord> mDisabledByAdmin = new ArraySet<>();
private ArrayList<UserRecord> mUsers = new ArrayList<>();
@VisibleForTesting
@@ -334,7 +325,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
for (UserInfo info : infos) {
boolean isCurrent = currentId == info.id;
- boolean switchToEnabled = canSwitchUsers || isCurrent;
if (!mUserSwitcherEnabled && !info.isPrimary()) {
continue;
}
@@ -343,25 +333,22 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
if (info.isGuest()) {
// Tapping guest icon triggers remove and a user switch therefore
// the icon shouldn't be enabled even if the user is current
- guestRecord = new UserRecord(info, null /* picture */,
- true /* isGuest */, isCurrent, false /* isAddUser */,
- false /* isRestricted */, canSwitchUsers,
- false /* isAddSupervisedUser */);
+ guestRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ mUserManager,
+ null /* picture */,
+ info,
+ isCurrent,
+ canSwitchUsers);
} else if (info.supportsSwitchToByUser()) {
- Bitmap picture = bitmaps.get(info.id);
- if (picture == null) {
- picture = mUserManager.getUserIcon(info.id);
-
- if (picture != null) {
- int avatarSize = mContext.getResources()
- .getDimensionPixelSize(R.dimen.max_avatar_size);
- picture = Bitmap.createScaledBitmap(
- picture, avatarSize, avatarSize, true);
- }
- }
- records.add(new UserRecord(info, picture, false /* isGuest */,
- isCurrent, false /* isAddUser */, false /* isRestricted */,
- switchToEnabled, false /* isAddSupervisedUser */));
+ records.add(
+ LegacyUserDataHelper.createRecord(
+ mContext,
+ mUserManager,
+ bitmaps.get(info.id),
+ info,
+ isCurrent,
+ canSwitchUsers));
}
}
}
@@ -372,18 +359,20 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
// we will just use it as an indicator for "Resetting guest...".
// Otherwise, default to canSwitchUsers.
boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers;
- guestRecord = new UserRecord(null /* info */, null /* picture */,
- true /* isGuest */, false /* isCurrent */,
- false /* isAddUser */, false /* isRestricted */,
- isSwitchToGuestEnabled, false /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(guestRecord);
+ guestRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ENTER_GUEST_MODE,
+ false /* isRestricted */,
+ isSwitchToGuestEnabled);
records.add(guestRecord);
} else if (canCreateGuest(guestRecord != null)) {
- guestRecord = new UserRecord(null /* info */, null /* picture */,
- true /* isGuest */, false /* isCurrent */,
- false /* isAddUser */, createIsRestricted(), canSwitchUsers,
- false /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(guestRecord);
+ guestRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ENTER_GUEST_MODE,
+ false /* isRestricted */,
+ canSwitchUsers);
records.add(guestRecord);
}
} else {
@@ -391,20 +380,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
}
if (canCreateUser()) {
- UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
- false /* isGuest */, false /* isCurrent */, true /* isAddUser */,
- createIsRestricted(), canSwitchUsers,
- false /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(addUserRecord);
- records.add(addUserRecord);
+ final UserRecord userRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ADD_USER,
+ createIsRestricted(),
+ canSwitchUsers);
+ records.add(userRecord);
}
if (canCreateSupervisedUser()) {
- UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
- false /* isGuest */, false /* isCurrent */, false /* isAddUser */,
- createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(addUserRecord);
- records.add(addUserRecord);
+ final UserRecord userRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ADD_SUPERVISED_USER,
+ createIsRestricted(),
+ canSwitchUsers);
+ records.add(userRecord);
}
mUiExecutor.execute(() -> {
@@ -591,12 +583,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower);
}
- private void showExitGuestDialog(int id, boolean isGuestEphemeral,
- int targetId, DialogShower dialogShower) {
+ private void showExitGuestDialog(
+ int id,
+ boolean isGuestEphemeral,
+ int targetId,
+ DialogShower dialogShower) {
if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
mExitGuestDialog.cancel();
}
- mExitGuestDialog = new ExitGuestDialog(mContext, id, isGuestEphemeral, targetId);
+ mExitGuestDialog = new ExitGuestDialog(
+ mContext,
+ id,
+ isGuestEphemeral,
+ targetId,
+ mKeyguardStateController.isShowing(),
+ mFalsingManager,
+ mDialogLaunchAnimator,
+ this::exitGuestUser);
if (dialogShower != null) {
dialogShower.showDialog(mExitGuestDialog, new DialogCuj(
InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
@@ -622,7 +625,15 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
mAddUserDialog.cancel();
}
- mAddUserDialog = new AddUserDialog(mContext);
+ final UserInfo currentUser = mUserTracker.getUserInfo();
+ mAddUserDialog = new AddUserDialog(
+ mContext,
+ currentUser.getUserHandle(),
+ mKeyguardStateController.isShowing(),
+ /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(),
+ mFalsingManager,
+ mBroadcastSender,
+ mDialogLaunchAnimator);
if (dialogShower != null) {
dialogShower.showDialog(mAddUserDialog,
new DialogCuj(
@@ -964,30 +975,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
return mKeyguardStateController.isShowing();
}
- @Override
- @Nullable
- public EnforcedAdmin getEnforcedAdmin(UserRecord record) {
- return mEnforcedAdminByUserRecord.get(record);
- }
-
- @Override
- public boolean isDisabledByAdmin(UserRecord record) {
- return mDisabledByAdmin.contains(record);
- }
-
- private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) {
- EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
- UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId());
- if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
- UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId())) {
- mDisabledByAdmin.add(record);
- mEnforcedAdminByUserRecord.put(record, admin);
- } else {
- mDisabledByAdmin.remove(record);
- mEnforcedAdminByUserRecord.put(record, null);
- }
- }
-
private boolean shouldUseSimpleUserSwitcher() {
int defaultSimpleUserSwitcher = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0;
@@ -1052,133 +1039,4 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController {
}
}
};
-
-
- private final class ExitGuestDialog extends SystemUIDialog implements
- DialogInterface.OnClickListener {
-
- private final int mGuestId;
- private final int mTargetId;
- private final boolean mIsGuestEphemeral;
-
- ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral,
- int targetId) {
- super(context);
- if (isGuestEphemeral) {
- setTitle(context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_title));
- setMessage(context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_message));
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_button), this);
- } else {
- setTitle(context.getString(
- com.android.settingslib
- .R.string.guest_exit_dialog_title_non_ephemeral));
- setMessage(context.getString(
- com.android.settingslib
- .R.string.guest_exit_dialog_message_non_ephemeral));
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_NEGATIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_clear_data_button),
- this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_save_data_button),
- this);
- }
- SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
- setCanceledOnTouchOutside(false);
- mGuestId = guestId;
- mTargetId = targetId;
- mIsGuestEphemeral = isGuestEphemeral;
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY
- : FalsingManager.HIGH_PENALTY;
- if (mFalsingManager.isFalseTap(penalty)) {
- return;
- }
- if (mIsGuestEphemeral) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Ephemeral guest: exit guest, guest is removed by the system
- // on exit, since its marked ephemeral
- exitGuestUser(mGuestId, mTargetId, false);
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- // Cancel clicked, do nothing
- cancel();
- }
- } else {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Non-ephemeral guest: exit guest, guest is not removed by the system
- // on exit, since its marked non-ephemeral
- exitGuestUser(mGuestId, mTargetId, false);
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Non-ephemeral guest: remove guest and then exit
- exitGuestUser(mGuestId, mTargetId, true);
- } else if (which == DialogInterface.BUTTON_NEUTRAL) {
- // Cancel clicked, do nothing
- cancel();
- }
- }
- }
- }
-
- @VisibleForTesting
- final class AddUserDialog extends SystemUIDialog implements
- DialogInterface.OnClickListener {
-
- AddUserDialog(Context context) {
- super(context);
-
- setTitle(com.android.settingslib.R.string.user_add_user_title);
- String message = context.getString(
- com.android.settingslib.R.string.user_add_user_message_short);
- UserInfo currentUser = mUserTracker.getUserInfo();
- if (currentUser != null && currentUser.isGuest() && currentUser.isEphemeral()) {
- message += context.getString(R.string.user_add_user_message_guest_remove);
- }
- setMessage(message);
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(android.R.string.ok), this);
- SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY
- : FalsingManager.MODERATE_PENALTY;
- if (mFalsingManager.isFalseTap(penalty)) {
- return;
- }
- if (which == BUTTON_NEUTRAL) {
- cancel();
- } else {
- mDialogLaunchAnimator.dismissStack(this);
- if (ActivityManager.isUserAMonkey()) {
- return;
- }
- // Use broadcast instead of ShadeController, as this dialog may have started in
- // another process and normal dagger bindings are not available
- mBroadcastSender.sendBroadcastAsUser(
- new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT);
- getContext().startActivityAsUser(
- CreateUserActivity.createIntentForStart(getContext()),
- mUserTracker.getUserHandle());
- }
- }
- }
-
}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
new file mode 100644
index 000000000000..9c38dc0f8852
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.systemui.telephony.data.repository
+
+import android.telephony.Annotation
+import android.telephony.TelephonyCallback
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.telephony.TelephonyListenerManager
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Defines interface for classes that encapsulate _some_ telephony-related state. */
+interface TelephonyRepository {
+ /** The state of the current call. */
+ @Annotation.CallState val callState: Flow<Int>
+}
+
+/**
+ * NOTE: This repository tracks only telephony-related state regarding the default mobile
+ * subscription. `TelephonyListenerManager` does not create new instances of `TelephonyManager` on a
+ * per-subscription basis and thus will always be tracking telephony information regarding
+ * `SubscriptionManager.getDefaultSubscriptionId`. See `TelephonyManager` and `SubscriptionManager`
+ * for more documentation.
+ */
+@SysUISingleton
+class TelephonyRepositoryImpl
+@Inject
+constructor(
+ private val manager: TelephonyListenerManager,
+) : TelephonyRepository {
+ @Annotation.CallState
+ override val callState: Flow<Int> = conflatedCallbackFlow {
+ val listener = TelephonyCallback.CallStateListener { state -> trySend(state) }
+
+ manager.addCallStateListener(listener)
+
+ awaitClose { manager.removeCallStateListener(listener) }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt
new file mode 100644
index 000000000000..630fbf2d1a07
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.systemui.telephony.data.repository
+
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface TelephonyRepositoryModule {
+ @Binds fun repository(impl: TelephonyRepositoryImpl): TelephonyRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
new file mode 100644
index 000000000000..86ca33df24dd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.systemui.telephony.domain.interactor
+
+import android.telephony.Annotation
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.telephony.data.repository.TelephonyRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Hosts business logic related to telephony. */
+@SysUISingleton
+class TelephonyInteractor
+@Inject
+constructor(
+ repository: TelephonyRepository,
+) {
+ @Annotation.CallState val callState: Flow<Int> = repository.callState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
index 5b522dcc4885..0c72b78a3c46 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java
+++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
@@ -20,6 +20,7 @@ import android.app.Activity;
import com.android.settingslib.users.EditUserInfoController;
import com.android.systemui.user.data.repository.UserRepositoryModule;
+import com.android.systemui.user.ui.dialog.UserDialogModule;
import dagger.Binds;
import dagger.Module;
@@ -32,6 +33,7 @@ import dagger.multibindings.IntoMap;
*/
@Module(
includes = {
+ UserDialogModule.class,
UserRepositoryModule.class,
}
)
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt
new file mode 100644
index 000000000000..4fd55c0e21c8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.systemui.user.data.model
+
+/** Encapsulates the state of settings related to user switching. */
+data class UserSwitcherSettingsModel(
+ val isSimpleUserSwitcher: Boolean = false,
+ val isAddUsersFromLockscreen: Boolean = false,
+ val isUserSwitcherEnabled: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 035638800f9c..3014f39c17f8 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -18,9 +18,13 @@
package com.android.systemui.user.data.repository
import android.content.Context
+import android.content.pm.UserInfo
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
+import android.os.UserHandle
import android.os.UserManager
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import com.android.internal.util.UserIcons
import com.android.systemui.R
@@ -29,15 +33,36 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
import com.android.systemui.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* Acts as source of truth for user related data.
@@ -55,6 +80,18 @@ interface UserRepository {
/** List of available user-related actions. */
val actions: Flow<List<UserActionModel>>
+ /** User switcher related settings. */
+ val userSwitcherSettings: Flow<UserSwitcherSettingsModel>
+
+ /** List of all users on the device. */
+ val userInfos: Flow<List<UserInfo>>
+
+ /** [UserInfo] of the currently-selected user. */
+ val selectedUserInfo: Flow<UserInfo>
+
+ /** User ID of the last non-guest selected user. */
+ val lastSelectedNonGuestUserId: Int
+
/** Whether actions are available even when locked. */
val isActionableWhenLocked: Flow<Boolean>
@@ -62,7 +99,23 @@ interface UserRepository {
val isGuestUserAutoCreated: Boolean
/** Whether the guest user is currently being reset. */
- val isGuestUserResetting: Boolean
+ var isGuestUserResetting: Boolean
+
+ /** Whether we've scheduled the creation of a guest user. */
+ val isGuestUserCreationScheduled: AtomicBoolean
+
+ /** The user of the secondary service. */
+ var secondaryUserId: Int
+
+ /** Whether refresh users should be paused. */
+ var isRefreshUsersPaused: Boolean
+
+ /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */
+ fun refreshUsers()
+
+ fun getSelectedUserInfo(): UserInfo
+
+ fun isSimpleUserSwitcher(): Boolean
}
@SysUISingleton
@@ -71,9 +124,31 @@ class UserRepositoryImpl
constructor(
@Application private val appContext: Context,
private val manager: UserManager,
- controller: UserSwitcherController,
+ private val controller: UserSwitcherController,
+ @Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val globalSettings: GlobalSettings,
+ private val tracker: UserTracker,
+ private val featureFlags: FeatureFlags,
) : UserRepository {
+ private val isNewImpl: Boolean
+ get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
+ private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null)
+ override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
+ _userSwitcherSettings.asStateFlow().filterNotNull()
+
+ private val _userInfos = MutableStateFlow<List<UserInfo>?>(null)
+ override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull()
+
+ private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
+ override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+
+ override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+ private set
+
private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
fun send() {
trySendWithFailureLogging(
@@ -99,11 +174,148 @@ constructor(
override val actions: Flow<List<UserActionModel>> =
userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }
- override val isActionableWhenLocked: Flow<Boolean> = controller.isAddUsersFromLockScreenEnabled
+ override val isActionableWhenLocked: Flow<Boolean> =
+ if (isNewImpl) {
+ emptyFlow()
+ } else {
+ controller.isAddUsersFromLockScreenEnabled
+ }
+
+ override val isGuestUserAutoCreated: Boolean =
+ if (isNewImpl) {
+ appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated)
+ } else {
+ controller.isGuestUserAutoCreated
+ }
+
+ private var _isGuestUserResetting: Boolean = false
+ override var isGuestUserResetting: Boolean =
+ if (isNewImpl) {
+ _isGuestUserResetting
+ } else {
+ controller.isGuestUserResetting
+ }
+ set(value) =
+ if (isNewImpl) {
+ _isGuestUserResetting = value
+ } else {
+ error("Not supported in the old implementation!")
+ }
+
+ override val isGuestUserCreationScheduled = AtomicBoolean()
+
+ override var secondaryUserId: Int = UserHandle.USER_NULL
- override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated
+ override var isRefreshUsersPaused: Boolean = false
- override val isGuestUserResetting: Boolean = controller.isGuestUserResetting
+ init {
+ if (isNewImpl) {
+ observeSelectedUser()
+ observeUserSettings()
+ }
+ }
+
+ override fun refreshUsers() {
+ applicationScope.launch {
+ val result = withContext(backgroundDispatcher) { manager.aliveUsers }
+
+ if (result != null) {
+ _userInfos.value = result
+ }
+ }
+ }
+
+ override fun getSelectedUserInfo(): UserInfo {
+ return checkNotNull(_selectedUserInfo.value)
+ }
+
+ override fun isSimpleUserSwitcher(): Boolean {
+ return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher)
+ }
+
+ private fun observeSelectedUser() {
+ conflatedCallbackFlow {
+ fun send() {
+ trySendWithFailureLogging(tracker.userInfo, TAG)
+ }
+
+ val callback =
+ object : UserTracker.Callback {
+ override fun onUserChanged(newUser: Int, userContext: Context) {
+ send()
+ }
+ }
+
+ tracker.addCallback(callback, mainDispatcher.asExecutor())
+ send()
+
+ awaitClose { tracker.removeCallback(callback) }
+ }
+ .onEach {
+ if (!it.isGuest) {
+ lastSelectedNonGuestUserId = it.id
+ }
+
+ _selectedUserInfo.value = it
+ }
+ .launchIn(applicationScope)
+ }
+
+ private fun observeUserSettings() {
+ globalSettings
+ .observerFlow(
+ names =
+ arrayOf(
+ SETTING_SIMPLE_USER_SWITCHER,
+ Settings.Global.ADD_USERS_WHEN_LOCKED,
+ Settings.Global.USER_SWITCHER_ENABLED,
+ ),
+ userId = UserHandle.USER_SYSTEM,
+ )
+ .onStart { emit(Unit) } // Forces an initial update.
+ .map { getSettings() }
+ .onEach { _userSwitcherSettings.value = it }
+ .launchIn(applicationScope)
+ }
+
+ private suspend fun getSettings(): UserSwitcherSettingsModel {
+ return withContext(backgroundDispatcher) {
+ val isSimpleUserSwitcher =
+ globalSettings.getIntForUser(
+ SETTING_SIMPLE_USER_SWITCHER,
+ if (
+ appContext.resources.getBoolean(
+ com.android.internal.R.bool.config_expandLockScreenUserSwitcher
+ )
+ ) {
+ 1
+ } else {
+ 0
+ },
+ UserHandle.USER_SYSTEM,
+ ) != 0
+
+ val isAddUsersFromLockscreen =
+ globalSettings.getIntForUser(
+ Settings.Global.ADD_USERS_WHEN_LOCKED,
+ 0,
+ UserHandle.USER_SYSTEM,
+ ) != 0
+
+ val isUserSwitcherEnabled =
+ globalSettings.getIntForUser(
+ Settings.Global.USER_SWITCHER_ENABLED,
+ 0,
+ UserHandle.USER_SYSTEM,
+ ) != 0
+
+ UserSwitcherSettingsModel(
+ isSimpleUserSwitcher = isSimpleUserSwitcher,
+ isAddUsersFromLockscreen = isAddUsersFromLockscreen,
+ isUserSwitcherEnabled = isUserSwitcherEnabled,
+ )
+ }
+ }
private fun UserRecord.isUser(): Boolean {
return when {
@@ -125,6 +337,7 @@ constructor(
image = getUserImage(this),
isSelected = isCurrent,
isSelectable = isSwitchToEnabled || isGuest,
+ isGuest = isGuest,
)
}
@@ -162,5 +375,6 @@ constructor(
companion object {
private const val TAG = "UserRepository"
+ @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
index cf6da9a60d78..9370286d7ee7 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
@@ -19,6 +19,7 @@ package com.android.systemui.user.data.source
import android.content.pm.UserInfo
import android.graphics.Bitmap
import android.os.UserHandle
+import com.android.settingslib.RestrictedLockUtils
/** Encapsulates raw data for a user or an option item related to managing users on the device. */
data class UserRecord(
@@ -41,6 +42,11 @@ data class UserRecord(
@JvmField val isSwitchToEnabled: Boolean = false,
/** Whether this record represents an option to add another supervised user to the device. */
@JvmField val isAddSupervisedUser: Boolean = false,
+ /**
+ * An enforcing admin, if the user action represented by this record is disabled by the admin.
+ * If not disabled, this is `null`.
+ */
+ @JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null,
) {
/** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
@@ -59,6 +65,14 @@ data class UserRecord(
}
}
+ /**
+ * Returns `true` if the user action represented by this record has been disabled by an admin;
+ * `false` otherwise.
+ */
+ fun isDisabledByAdmin(): Boolean {
+ return enforcedAdmin != null
+ }
+
companion object {
@JvmStatic
fun createForGuest(): UserRecord {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
new file mode 100644
index 000000000000..07e5cf9d9df2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
@@ -0,0 +1,322 @@
+/*
+ * 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.systemui.user.domain.interactor
+
+import android.annotation.UserIdInt
+import android.app.admin.DevicePolicyManager
+import android.content.Context
+import android.content.pm.UserInfo
+import android.os.RemoteException
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import android.view.WindowManagerGlobal
+import android.widget.Toast
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.QSUserSwitcherEvent
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+/** Encapsulates business logic to interact with guest user data and systems. */
+@SysUISingleton
+class GuestUserInteractor
+@Inject
+constructor(
+ @Application private val applicationContext: Context,
+ @Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val manager: UserManager,
+ private val repository: UserRepository,
+ private val deviceProvisionedController: DeviceProvisionedController,
+ private val devicePolicyManager: DevicePolicyManager,
+ private val refreshUsersScheduler: RefreshUsersScheduler,
+ private val uiEventLogger: UiEventLogger,
+) {
+ /** Whether the device is configured to always have a guest user available. */
+ val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
+
+ /** Whether the guest user is currently being reset. */
+ val isGuestUserResetting: Boolean = repository.isGuestUserResetting
+
+ /** Notifies that the device has finished booting. */
+ fun onDeviceBootCompleted() {
+ applicationScope.launch {
+ if (isDeviceAllowedToAddGuest()) {
+ guaranteePresent()
+ return@launch
+ }
+
+ suspendCancellableCoroutine<Unit> { continuation ->
+ val callback =
+ object : DeviceProvisionedController.DeviceProvisionedListener {
+ override fun onDeviceProvisionedChanged() {
+ continuation.resumeWith(Result.success(Unit))
+ deviceProvisionedController.removeCallback(this)
+ }
+ }
+
+ deviceProvisionedController.addCallback(callback)
+ }
+
+ if (isDeviceAllowedToAddGuest()) {
+ guaranteePresent()
+ }
+ }
+ }
+
+ /** Creates a guest user and switches to it. */
+ fun createAndSwitchTo(
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ selectUser: (userId: Int) -> Unit,
+ ) {
+ applicationScope.launch {
+ val newGuestUserId = create(showDialog, dismissDialog)
+ if (newGuestUserId != UserHandle.USER_NULL) {
+ selectUser(newGuestUserId)
+ }
+ }
+ }
+
+ /** Exits the guest user, switching back to the last non-guest user or to the default user. */
+ fun exit(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ forceRemoveGuestOnExit: Boolean,
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ switchUser: (userId: Int) -> Unit,
+ ) {
+ val currentUserInfo = repository.getSelectedUserInfo()
+ if (currentUserInfo.id != guestUserId) {
+ Log.w(
+ TAG,
+ "User requesting to start a new session ($guestUserId) is not current user" +
+ " (${currentUserInfo.id})"
+ )
+ return
+ }
+
+ if (!currentUserInfo.isGuest) {
+ Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
+ return
+ }
+
+ applicationScope.launch {
+ var newUserId = UserHandle.USER_SYSTEM
+ if (targetUserId == UserHandle.USER_NULL) {
+ // When a target user is not specified switch to last non guest user:
+ val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId
+ if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) {
+ val info =
+ withContext(backgroundDispatcher) {
+ manager.getUserInfo(lastSelectedNonGuestUserHandle)
+ }
+ if (info != null && info.isEnabled && info.supportsSwitchToByUser()) {
+ newUserId = info.id
+ }
+ }
+ } else {
+ newUserId = targetUserId
+ }
+
+ if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) {
+ uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE)
+ remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser)
+ } else {
+ uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH)
+ switchUser(newUserId)
+ }
+ }
+ }
+
+ /**
+ * Guarantees that the guest user is present on the device, creating it if needed and if allowed
+ * to.
+ */
+ suspend fun guaranteePresent() {
+ if (!isDeviceAllowedToAddGuest()) {
+ return
+ }
+
+ val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() }
+ if (guestUser == null) {
+ scheduleCreation()
+ }
+ }
+
+ /** Removes the guest user from the device. */
+ suspend fun remove(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ switchUser: (userId: Int) -> Unit,
+ ) {
+ val currentUser: UserInfo = repository.getSelectedUserInfo()
+ if (currentUser.id != guestUserId) {
+ Log.w(
+ TAG,
+ "User requesting to start a new session ($guestUserId) is not current user" +
+ " ($currentUser.id)"
+ )
+ return
+ }
+
+ if (!currentUser.isGuest) {
+ Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
+ return
+ }
+
+ val marked =
+ withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) }
+ if (!marked) {
+ Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId")
+ return
+ }
+
+ if (targetUserId == UserHandle.USER_NULL) {
+ // Create a new guest in the foreground, and then immediately switch to it
+ val newGuestId = create(showDialog, dismissDialog)
+ if (newGuestId == UserHandle.USER_NULL) {
+ Log.e(TAG, "Could not create new guest, switching back to system user")
+ switchUser(UserHandle.USER_SYSTEM)
+ withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
+ try {
+ WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null)
+ } catch (e: RemoteException) {
+ Log.e(
+ TAG,
+ "Couldn't remove guest because ActivityManager or WindowManager is dead"
+ )
+ }
+ return
+ }
+
+ switchUser(newGuestId)
+
+ withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
+ } else {
+ if (repository.isGuestUserAutoCreated) {
+ repository.isGuestUserResetting = true
+ }
+ switchUser(targetUserId)
+ manager.removeUser(currentUser.id)
+ }
+ }
+
+ /**
+ * Creates the guest user and adds it to the device.
+ *
+ * @param showDialog A function to invoke to show a dialog.
+ * @param dismissDialog A function to invoke to dismiss a dialog.
+ * @return The user ID of the newly-created guest user.
+ */
+ private suspend fun create(
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ ): Int {
+ return withContext(mainDispatcher) {
+ showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+ val guestUserId = createInBackground()
+ dismissDialog()
+ if (guestUserId != UserHandle.USER_NULL) {
+ uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD)
+ } else {
+ Toast.makeText(
+ applicationContext,
+ com.android.settingslib.R.string.add_guest_failed,
+ Toast.LENGTH_SHORT,
+ )
+ .show()
+ }
+
+ guestUserId
+ }
+ }
+
+ /** Schedules the creation of the guest user. */
+ private suspend fun scheduleCreation() {
+ if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) {
+ return
+ }
+
+ withContext(backgroundDispatcher) {
+ val newGuestUserId = createInBackground()
+ repository.isGuestUserCreationScheduled.set(false)
+ repository.isGuestUserResetting = false
+ if (newGuestUserId == UserHandle.USER_NULL) {
+ Log.w(TAG, "Could not create new guest while exiting existing guest")
+ // Refresh users so that we still display "Guest" if
+ // config_guestUserAutoCreated=true
+ refreshUsersScheduler.refreshIfNotPaused()
+ }
+ }
+ }
+
+ /**
+ * Creates a guest user and return its multi-user user ID.
+ *
+ * This method does not check if a guest already exists before it makes a call to [UserManager]
+ * to create a new one.
+ *
+ * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if
+ * the guest couldn't be created.
+ */
+ @UserIdInt
+ private suspend fun createInBackground(): Int {
+ return withContext(backgroundDispatcher) {
+ try {
+ val guestUser = manager.createGuest(applicationContext)
+ if (guestUser != null) {
+ guestUser.id
+ } else {
+ Log.e(
+ TAG,
+ "Couldn't create guest, most likely because there already exists one!"
+ )
+ UserHandle.USER_NULL
+ }
+ } catch (e: UserManager.UserOperationException) {
+ Log.e(TAG, "Couldn't create guest user!", e)
+ UserHandle.USER_NULL
+ }
+ }
+ }
+
+ private fun isDeviceAllowedToAddGuest(): Boolean {
+ return deviceProvisionedController.isDeviceProvisioned &&
+ !devicePolicyManager.isDeviceManaged
+ }
+
+ companion object {
+ private const val TAG = "GuestUserInteractor"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt
new file mode 100644
index 000000000000..8f36821a955e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.systemui.user.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/** Encapsulates logic for pausing, unpausing, and scheduling a delayed job. */
+@SysUISingleton
+class RefreshUsersScheduler
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ private val repository: UserRepository,
+) {
+ private var scheduledUnpauseJob: Job? = null
+ private var isPaused = false
+
+ fun pause() {
+ applicationScope.launch(mainDispatcher) {
+ isPaused = true
+ scheduledUnpauseJob?.cancel()
+ scheduledUnpauseJob =
+ applicationScope.launch {
+ delay(PAUSE_REFRESH_USERS_TIMEOUT_MS)
+ unpauseAndRefresh()
+ }
+ }
+ }
+
+ fun unpauseAndRefresh() {
+ applicationScope.launch(mainDispatcher) {
+ isPaused = false
+ refreshIfNotPaused()
+ }
+ }
+
+ fun refreshIfNotPaused() {
+ applicationScope.launch(mainDispatcher) {
+ if (isPaused) {
+ return@launch
+ }
+
+ repository.refreshUsers()
+ }
+ }
+
+ companion object {
+ private const val PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
new file mode 100644
index 000000000000..1b4746a99f8f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.systemui.user.domain.interactor
+
+import android.os.UserHandle
+import android.os.UserManager
+import com.android.systemui.user.data.repository.UserRepository
+
+/** Utilities related to user management actions. */
+object UserActionsUtil {
+
+ /** Returns `true` if it's possible to add a guest user to the device; `false` otherwise. */
+ fun canCreateGuest(
+ manager: UserManager,
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ if (!isUserSwitcherEnabled) {
+ return false
+ }
+
+ return currentUserCanCreateUsers(manager, repository) ||
+ anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled)
+ }
+
+ /** Returns `true` if it's possible to add a user to the device; `false` otherwise. */
+ fun canCreateUser(
+ manager: UserManager,
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ if (!isUserSwitcherEnabled) {
+ return false
+ }
+
+ if (
+ !currentUserCanCreateUsers(manager, repository) &&
+ !anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled)
+ ) {
+ return false
+ }
+
+ return manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY)
+ }
+
+ /**
+ * Returns `true` if it's possible to add a supervised user to the device; `false` otherwise.
+ */
+ fun canCreateSupervisedUser(
+ manager: UserManager,
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ supervisedUserPackageName: String?
+ ): Boolean {
+ if (supervisedUserPackageName.isNullOrEmpty()) {
+ return false
+ }
+
+ return canCreateUser(
+ manager,
+ repository,
+ isUserSwitcherEnabled,
+ isAddUsersFromLockScreenEnabled
+ )
+ }
+
+ /**
+ * Returns `true` if the current user is allowed to add users to the device; `false` otherwise.
+ */
+ private fun currentUserCanCreateUsers(
+ manager: UserManager,
+ repository: UserRepository,
+ ): Boolean {
+ val currentUser = repository.getSelectedUserInfo()
+ if (!currentUser.isAdmin && currentUser.id != UserHandle.USER_SYSTEM) {
+ return false
+ }
+
+ return systemCanCreateUsers(manager)
+ }
+
+ /** Returns `true` if the system can add users to the device; `false` otherwise. */
+ private fun systemCanCreateUsers(
+ manager: UserManager,
+ ): Boolean {
+ return !manager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM)
+ }
+
+ /** Returns `true` if it's allowed to add users to the device at all; `false` otherwise. */
+ private fun anyoneCanCreateUsers(
+ manager: UserManager,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ return systemCanCreateUsers(manager) && isAddUsersFromLockScreenEnabled
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index 3c5b9697c013..a84238c1559a 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -17,94 +17,725 @@
package com.android.systemui.user.domain.interactor
+import android.annotation.SuppressLint
+import android.annotation.UserIdInt
+import android.app.ActivityManager
+import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.UserInfo
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.RemoteException
+import android.os.UserHandle
+import android.os.UserManager
import android.provider.Settings
+import android.util.Log
+import com.android.internal.util.UserIcons
+import com.android.systemui.R
+import com.android.systemui.SystemUISecondaryUserService
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.kotlin.pairwise
+import java.io.PrintWriter
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
/** Encapsulates business logic to interact with user data and systems. */
@SysUISingleton
class UserInteractor
@Inject
constructor(
- repository: UserRepository,
+ @Application private val applicationContext: Context,
+ private val repository: UserRepository,
private val controller: UserSwitcherController,
private val activityStarter: ActivityStarter,
- keyguardInteractor: KeyguardInteractor,
+ private val keyguardInteractor: KeyguardInteractor,
+ private val featureFlags: FeatureFlags,
+ private val manager: UserManager,
+ @Application private val applicationScope: CoroutineScope,
+ telephonyInteractor: TelephonyInteractor,
+ broadcastDispatcher: BroadcastDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val activityManager: ActivityManager,
+ private val refreshUsersScheduler: RefreshUsersScheduler,
+ private val guestUserInteractor: GuestUserInteractor,
) {
+ /**
+ * Defines interface for classes that can be notified when the state of users on the device is
+ * changed.
+ */
+ interface UserCallback {
+ /** Returns `true` if this callback can be cleaned-up. */
+ fun isEvictable(): Boolean = false
+ /** Notifies that the state of users on the device has changed. */
+ fun onUserStateChanged()
+ }
+
+ private val isNewImpl: Boolean
+ get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
+ private val supervisedUserPackageName: String?
+ get() =
+ applicationContext.getString(
+ com.android.internal.R.string.config_supervisedUserCreationPackage
+ )
+
+ private val callbackMutex = Mutex()
+ private val callbacks = mutableSetOf<UserCallback>()
+
/** List of current on-device users to select from. */
- val users: Flow<List<UserModel>> = repository.users
+ val users: Flow<List<UserModel>>
+ get() =
+ if (isNewImpl) {
+ combine(
+ repository.userInfos,
+ repository.selectedUserInfo,
+ repository.userSwitcherSettings,
+ ) { userInfos, selectedUserInfo, settings ->
+ toUserModels(
+ userInfos = userInfos,
+ selectedUserId = selectedUserInfo.id,
+ isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
+ )
+ }
+ } else {
+ repository.users
+ }
/** The currently-selected user. */
- val selectedUser: Flow<UserModel> = repository.selectedUser
+ val selectedUser: Flow<UserModel>
+ get() =
+ if (isNewImpl) {
+ combine(
+ repository.selectedUserInfo,
+ repository.userSwitcherSettings,
+ ) { selectedUserInfo, settings ->
+ val selectedUserId = selectedUserInfo.id
+ checkNotNull(
+ toUserModel(
+ userInfo = selectedUserInfo,
+ selectedUserId = selectedUserId,
+ canSwitchUsers = canSwitchUsers(selectedUserId),
+ isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
+ )
+ )
+ }
+ } else {
+ repository.selectedUser
+ }
/** List of user-switcher related actions that are available. */
- val actions: Flow<List<UserActionModel>> =
- combine(
- repository.isActionableWhenLocked,
- keyguardInteractor.isKeyguardShowing,
- ) { isActionableWhenLocked, isLocked ->
- isActionableWhenLocked || !isLocked
- }
- .flatMapLatest { isActionable ->
- if (isActionable) {
- repository.actions.map { actions ->
- actions +
- if (actions.isNotEmpty()) {
- // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because
- // that's a user
- // switcher specific action that is not known to the our data source
- // or other
- // features.
- listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
- } else {
- // If no actions, don't add the navigate action.
- emptyList()
- }
+ val actions: Flow<List<UserActionModel>>
+ get() =
+ if (isNewImpl) {
+ combine(
+ repository.userInfos,
+ repository.userSwitcherSettings,
+ keyguardInteractor.isKeyguardShowing,
+ ) { userInfos, settings, isDeviceLocked ->
+ buildList {
+ val hasGuestUser = userInfos.any { it.isGuest }
+ if (
+ !hasGuestUser &&
+ (guestUserInteractor.isGuestUserAutoCreated ||
+ UserActionsUtil.canCreateGuest(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ ))
+ ) {
+ add(UserActionModel.ENTER_GUEST_MODE)
+ }
+
+ if (isDeviceLocked && !settings.isAddUsersFromLockscreen) {
+ // The device is locked and our setting to allow actions that add users
+ // from the lock-screen is not enabled. The guest action from above is
+ // always allowed, even when the device is locked, but the various "add
+ // user" actions below are not. We can finish building the list here.
+ return@buildList
+ }
+
+ if (
+ UserActionsUtil.canCreateUser(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ )
+ ) {
+ add(UserActionModel.ADD_USER)
+ }
+
+ if (
+ UserActionsUtil.canCreateSupervisedUser(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ supervisedUserPackageName,
+ )
+ ) {
+ add(UserActionModel.ADD_SUPERVISED_USER)
+ }
}
- } else {
- // If not actionable it means that we're not allowed to show actions when locked
- // and we
- // are locked. Therefore, we should show no actions.
- flowOf(emptyList())
}
+ } else {
+ combine(
+ repository.isActionableWhenLocked,
+ keyguardInteractor.isKeyguardShowing,
+ ) { isActionableWhenLocked, isLocked ->
+ isActionableWhenLocked || !isLocked
+ }
+ .flatMapLatest { isActionable ->
+ if (isActionable) {
+ repository.actions.map { actions ->
+ actions +
+ if (actions.isNotEmpty()) {
+ // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT
+ // because that's a user switcher specific action that is
+ // not known to the our data source or other features.
+ listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+ } else {
+ // If no actions, don't add the navigate action.
+ emptyList()
+ }
+ }
+ } else {
+ // If not actionable it means that we're not allowed to show actions
+ // when
+ // locked and we are locked. Therefore, we should show no actions.
+ flowOf(emptyList())
+ }
+ }
}
+ val userRecords: StateFlow<ArrayList<UserRecord>> =
+ if (isNewImpl) {
+ combine(
+ repository.userInfos,
+ repository.selectedUserInfo,
+ actions,
+ repository.userSwitcherSettings,
+ ) { userInfos, selectedUserInfo, actionModels, settings ->
+ ArrayList(
+ userInfos.map {
+ toRecord(
+ userInfo = it,
+ selectedUserId = selectedUserInfo.id,
+ )
+ } +
+ actionModels.map {
+ toRecord(
+ action = it,
+ selectedUserId = selectedUserInfo.id,
+ isAddFromLockscreenEnabled = settings.isAddUsersFromLockscreen,
+ )
+ }
+ )
+ }
+ .onEach { notifyCallbacks() }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = ArrayList(),
+ )
+ } else {
+ MutableStateFlow(ArrayList())
+ }
+
+ val selectedUserRecord: StateFlow<UserRecord?> =
+ if (isNewImpl) {
+ repository.selectedUserInfo
+ .map { selectedUserInfo ->
+ toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id)
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = null,
+ )
+ } else {
+ MutableStateFlow(null)
+ }
+
/** Whether the device is configured to always have a guest user available. */
- val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
+ val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated
/** Whether the guest user is currently being reset. */
- val isGuestUserResetting: Boolean = repository.isGuestUserResetting
+ val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting
+
+ private val _dialogShowRequests = MutableStateFlow<ShowDialogRequestModel?>(null)
+ val dialogShowRequests: Flow<ShowDialogRequestModel?> = _dialogShowRequests.asStateFlow()
+
+ private val _dialogDismissRequests = MutableStateFlow<Unit?>(null)
+ val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow()
+
+ val isSimpleUserSwitcher: Boolean
+ get() =
+ if (isNewImpl) {
+ repository.isSimpleUserSwitcher()
+ } else {
+ error("Not supported in the old implementation!")
+ }
+
+ init {
+ if (isNewImpl) {
+ refreshUsersScheduler.refreshIfNotPaused()
+ telephonyInteractor.callState
+ .distinctUntilChanged()
+ .onEach { refreshUsersScheduler.refreshIfNotPaused() }
+ .launchIn(applicationScope)
+
+ combine(
+ broadcastDispatcher.broadcastFlow(
+ filter =
+ IntentFilter().apply {
+ addAction(Intent.ACTION_USER_ADDED)
+ addAction(Intent.ACTION_USER_REMOVED)
+ addAction(Intent.ACTION_USER_INFO_CHANGED)
+ addAction(Intent.ACTION_USER_SWITCHED)
+ addAction(Intent.ACTION_USER_STOPPED)
+ addAction(Intent.ACTION_USER_UNLOCKED)
+ },
+ user = UserHandle.SYSTEM,
+ map = { intent, _ -> intent },
+ ),
+ repository.selectedUserInfo.pairwise(null),
+ ) { intent, selectedUserChange ->
+ Pair(intent, selectedUserChange.previousValue)
+ }
+ .onEach { (intent, previousSelectedUser) ->
+ onBroadcastReceived(intent, previousSelectedUser)
+ }
+ .launchIn(applicationScope)
+ }
+ }
+
+ fun addCallback(callback: UserCallback) {
+ applicationScope.launch { callbackMutex.withLock { callbacks.add(callback) } }
+ }
+
+ fun removeCallback(callback: UserCallback) {
+ applicationScope.launch { callbackMutex.withLock { callbacks.remove(callback) } }
+ }
+
+ fun refreshUsers() {
+ refreshUsersScheduler.refreshIfNotPaused()
+ }
+
+ fun onDialogShown() {
+ _dialogShowRequests.value = null
+ }
+
+ fun onDialogDismissed() {
+ _dialogDismissRequests.value = null
+ }
+
+ fun dump(pw: PrintWriter) {
+ pw.println("UserInteractor state:")
+ pw.println(" lastSelectedNonGuestUserId=${repository.lastSelectedNonGuestUserId}")
+
+ val users = userRecords.value.filter { it.info != null }
+ pw.println(" userCount=${userRecords.value.count { LegacyUserDataHelper.isUser(it) }}")
+ for (i in users.indices) {
+ pw.println(" ${users[i]}")
+ }
+
+ val actions = userRecords.value.filter { it.info == null }
+ pw.println(" actionCount=${userRecords.value.count { !LegacyUserDataHelper.isUser(it) }}")
+ for (i in actions.indices) {
+ pw.println(" ${actions[i]}")
+ }
+
+ pw.println("isSimpleUserSwitcher=$isSimpleUserSwitcher")
+ pw.println("isGuestUserAutoCreated=$isGuestUserAutoCreated")
+ }
+
+ fun onDeviceBootCompleted() {
+ guestUserInteractor.onDeviceBootCompleted()
+ }
/** Switches to the user with the given user ID. */
fun selectUser(
- userId: Int,
+ newlySelectedUserId: Int,
) {
- controller.onUserSelected(userId, /* dialogShower= */ null)
+ if (isNewImpl) {
+ val currentlySelectedUserInfo = repository.getSelectedUserInfo()
+ if (
+ newlySelectedUserId == currentlySelectedUserInfo.id &&
+ currentlySelectedUserInfo.isGuest
+ ) {
+ // Here when clicking on the currently-selected guest user to leave guest mode
+ // and return to the previously-selected non-guest user.
+ showDialog(
+ ShowDialogRequestModel.ShowExitGuestDialog(
+ guestUserId = currentlySelectedUserInfo.id,
+ targetUserId = repository.lastSelectedNonGuestUserId,
+ isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
+ isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+ onExitGuestUser = this::exitGuestUser,
+ )
+ )
+ return
+ }
+
+ if (currentlySelectedUserInfo.isGuest) {
+ // Here when switching from guest to a non-guest user.
+ showDialog(
+ ShowDialogRequestModel.ShowExitGuestDialog(
+ guestUserId = currentlySelectedUserInfo.id,
+ targetUserId = newlySelectedUserId,
+ isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
+ isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+ onExitGuestUser = this::exitGuestUser,
+ )
+ )
+ return
+ }
+
+ switchUser(newlySelectedUserId)
+ } else {
+ controller.onUserSelected(newlySelectedUserId, /* dialogShower= */ null)
+ }
}
/** Executes the given action. */
fun executeAction(action: UserActionModel) {
- when (action) {
- UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
- UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
- UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
- activityStarter.startActivity(
- Intent(Settings.ACTION_USER_SETTINGS),
- /* dismissShade= */ false,
+ if (isNewImpl) {
+ when (action) {
+ UserActionModel.ENTER_GUEST_MODE ->
+ guestUserInteractor.createAndSwitchTo(
+ this::showDialog,
+ this::dismissDialog,
+ this::selectUser,
+ )
+ UserActionModel.ADD_USER -> {
+ val currentUser = repository.getSelectedUserInfo()
+ showDialog(
+ ShowDialogRequestModel.ShowAddUserDialog(
+ userHandle = currentUser.userHandle,
+ isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+ showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
+ )
+ )
+ }
+ UserActionModel.ADD_SUPERVISED_USER ->
+ activityStarter.startActivity(
+ Intent()
+ .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
+ .setPackage(supervisedUserPackageName)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ /* dismissShade= */ false,
+ )
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
+ activityStarter.startActivity(
+ Intent(Settings.ACTION_USER_SETTINGS),
+ /* dismissShade= */ false,
+ )
+ }
+ } else {
+ when (action) {
+ UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
+ UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
+ UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
+ activityStarter.startActivity(
+ Intent(Settings.ACTION_USER_SETTINGS),
+ /* dismissShade= */ false,
+ )
+ }
+ }
+ }
+
+ fun exitGuestUser(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ forceRemoveGuestOnExit: Boolean,
+ ) {
+ guestUserInteractor.exit(
+ guestUserId = guestUserId,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = forceRemoveGuestOnExit,
+ showDialog = this::showDialog,
+ dismissDialog = this::dismissDialog,
+ switchUser = this::switchUser,
+ )
+ }
+
+ fun removeGuestUser(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ ) {
+ applicationScope.launch {
+ guestUserInteractor.remove(
+ guestUserId = guestUserId,
+ targetUserId = targetUserId,
+ ::showDialog,
+ ::dismissDialog,
+ ::selectUser,
+ )
+ }
+ }
+
+ private fun showDialog(request: ShowDialogRequestModel) {
+ _dialogShowRequests.value = request
+ }
+
+ private fun dismissDialog() {
+ _dialogDismissRequests.value = Unit
+ }
+
+ private fun notifyCallbacks() {
+ applicationScope.launch {
+ callbackMutex.withLock {
+ val iterator = callbacks.iterator()
+ while (iterator.hasNext()) {
+ val callback = iterator.next()
+ if (!callback.isEvictable()) {
+ callback.onUserStateChanged()
+ } else {
+ iterator.remove()
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun toRecord(
+ userInfo: UserInfo,
+ selectedUserId: Int,
+ ): UserRecord {
+ return LegacyUserDataHelper.createRecord(
+ context = applicationContext,
+ manager = manager,
+ userInfo = userInfo,
+ picture = null,
+ isCurrent = userInfo.id == selectedUserId,
+ canSwitchUsers = canSwitchUsers(selectedUserId),
+ )
+ }
+
+ private suspend fun toRecord(
+ action: UserActionModel,
+ selectedUserId: Int,
+ isAddFromLockscreenEnabled: Boolean,
+ ): UserRecord {
+ return LegacyUserDataHelper.createRecord(
+ context = applicationContext,
+ selectedUserId = selectedUserId,
+ actionType = action,
+ isRestricted =
+ if (action == UserActionModel.ENTER_GUEST_MODE) {
+ // Entering guest mode is never restricted, so it's allowed to happen from the
+ // lockscreen even if the "add from lockscreen" system setting is off.
+ false
+ } else {
+ !isAddFromLockscreenEnabled
+ },
+ isSwitchToEnabled =
+ canSwitchUsers(selectedUserId) &&
+ // If the user is auto-created is must not be currently resetting.
+ !(isGuestUserAutoCreated && isGuestUserResetting),
+ )
+ }
+
+ private fun switchUser(userId: Int) {
+ // TODO(b/246631653): track jank and lantecy like in the old impl.
+ refreshUsersScheduler.pause()
+ try {
+ activityManager.switchUser(userId)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Couldn't switch user.", e)
+ }
+ }
+
+ private suspend fun onBroadcastReceived(
+ intent: Intent,
+ previousUserInfo: UserInfo?,
+ ) {
+ val shouldRefreshAllUsers =
+ when (intent.action) {
+ Intent.ACTION_USER_SWITCHED -> {
+ dismissDialog()
+ val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
+ if (previousUserInfo?.id != selectedUserId) {
+ notifyCallbacks()
+ restartSecondaryService(selectedUserId)
+ }
+ if (guestUserInteractor.isGuestUserAutoCreated) {
+ guestUserInteractor.guaranteePresent()
+ }
+ true
+ }
+ Intent.ACTION_USER_INFO_CHANGED -> true
+ Intent.ACTION_USER_UNLOCKED -> {
+ // If we unlocked the system user, we should refresh all users.
+ intent.getIntExtra(
+ Intent.EXTRA_USER_HANDLE,
+ UserHandle.USER_NULL,
+ ) == UserHandle.USER_SYSTEM
+ }
+ else -> true
+ }
+
+ if (shouldRefreshAllUsers) {
+ refreshUsersScheduler.unpauseAndRefresh()
+ }
+ }
+
+ private fun restartSecondaryService(@UserIdInt userId: Int) {
+ val intent = Intent(applicationContext, SystemUISecondaryUserService::class.java)
+ // Disconnect from the old secondary user's service
+ val secondaryUserId = repository.secondaryUserId
+ if (secondaryUserId != UserHandle.USER_NULL) {
+ applicationContext.stopServiceAsUser(
+ intent,
+ UserHandle.of(secondaryUserId),
+ )
+ repository.secondaryUserId = UserHandle.USER_NULL
+ }
+
+ // Connect to the new secondary user's service (purely to ensure that a persistent
+ // SystemUI application is created for that user)
+ if (userId != UserHandle.USER_SYSTEM) {
+ applicationContext.startServiceAsUser(
+ intent,
+ UserHandle.of(userId),
+ )
+ repository.secondaryUserId = userId
+ }
+ }
+
+ private suspend fun toUserModels(
+ userInfos: List<UserInfo>,
+ selectedUserId: Int,
+ isUserSwitcherEnabled: Boolean,
+ ): List<UserModel> {
+ val canSwitchUsers = canSwitchUsers(selectedUserId)
+
+ return userInfos
+ // The guest user should go in the last position.
+ .sortedBy { it.isGuest }
+ .mapNotNull { userInfo ->
+ toUserModel(
+ userInfo = userInfo,
+ selectedUserId = selectedUserId,
+ canSwitchUsers = canSwitchUsers,
+ isUserSwitcherEnabled = isUserSwitcherEnabled,
+ )
+ }
+ }
+
+ private suspend fun toUserModel(
+ userInfo: UserInfo,
+ selectedUserId: Int,
+ canSwitchUsers: Boolean,
+ isUserSwitcherEnabled: Boolean,
+ ): UserModel? {
+ val userId = userInfo.id
+ val isSelected = userId == selectedUserId
+
+ return when {
+ // When the user switcher is not enabled in settings, we only show the primary user.
+ !isUserSwitcherEnabled && !userInfo.isPrimary -> null
+
+ // We avoid showing disabled users.
+ !userInfo.isEnabled -> null
+ userInfo.isGuest ->
+ UserModel(
+ id = userId,
+ name = Text.Loaded(userInfo.name),
+ image =
+ getUserImage(
+ isGuest = true,
+ userId = userId,
+ ),
+ isSelected = isSelected,
+ isSelectable = canSwitchUsers,
+ isGuest = true,
)
+ userInfo.supportsSwitchToByUser() ->
+ UserModel(
+ id = userId,
+ name = Text.Loaded(userInfo.name),
+ image =
+ getUserImage(
+ isGuest = false,
+ userId = userId,
+ ),
+ isSelected = isSelected,
+ isSelectable = canSwitchUsers || isSelected,
+ isGuest = false,
+ )
+ else -> null
+ }
+ }
+
+ private suspend fun canSwitchUsers(selectedUserId: Int): Boolean {
+ return withContext(backgroundDispatcher) {
+ manager.getUserSwitchability(UserHandle.of(selectedUserId))
+ } == UserManager.SWITCHABILITY_STATUS_OK
+ }
+
+ @SuppressLint("UseCompatLoadingForDrawables")
+ private suspend fun getUserImage(
+ isGuest: Boolean,
+ userId: Int,
+ ): Drawable {
+ if (isGuest) {
+ return checkNotNull(applicationContext.getDrawable(R.drawable.ic_account_circle))
+ }
+
+ // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them.
+ // TODO(b/246631653): downscale the bitmaps to R.dimen.max_avatar_size if requested.
+ val userIcon = withContext(backgroundDispatcher) { manager.getUserIcon(userId) }
+ if (userIcon != null) {
+ return BitmapDrawable(userIcon)
}
+
+ return UserIcons.getDefaultUserIcon(
+ applicationContext.resources,
+ userId,
+ /* light= */ false
+ )
+ }
+
+ companion object {
+ private const val TAG = "UserInteractor"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
new file mode 100644
index 000000000000..08d7c5a26a25
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.systemui.user.domain.model
+
+import android.os.UserHandle
+
+/** Encapsulates a request to show a dialog. */
+sealed class ShowDialogRequestModel {
+ data class ShowAddUserDialog(
+ val userHandle: UserHandle,
+ val isKeyguardShowing: Boolean,
+ val showEphemeralMessage: Boolean,
+ ) : ShowDialogRequestModel()
+
+ data class ShowUserCreationDialog(
+ val isGuest: Boolean,
+ ) : ShowDialogRequestModel()
+
+ data class ShowExitGuestDialog(
+ val guestUserId: Int,
+ val targetUserId: Int,
+ val isGuestEphemeral: Boolean,
+ val isKeyguardShowing: Boolean,
+ val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit,
+ ) : ShowDialogRequestModel()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
new file mode 100644
index 000000000000..137de1544b2d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.systemui.user.legacyhelper.data
+
+import android.content.Context
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.os.UserManager
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
+import com.android.settingslib.RestrictedLockUtilsInternal
+import com.android.systemui.R
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.shared.model.UserActionModel
+
+/**
+ * Defines utility functions for helping with legacy data code for users.
+ *
+ * We need these to avoid code duplication between logic inside the UserSwitcherController and in
+ * modern architecture classes such as repositories, interactors, and view-models. If we ever
+ * simplify UserSwitcherController (or delete it), the code here could be moved into its call-sites.
+ */
+object LegacyUserDataHelper {
+
+ @JvmStatic
+ fun createRecord(
+ context: Context,
+ manager: UserManager,
+ picture: Bitmap?,
+ userInfo: UserInfo,
+ isCurrent: Boolean,
+ canSwitchUsers: Boolean,
+ ): UserRecord {
+ val isGuest = userInfo.isGuest
+ return UserRecord(
+ info = userInfo,
+ picture =
+ getPicture(
+ manager = manager,
+ context = context,
+ userInfo = userInfo,
+ picture = picture,
+ ),
+ isGuest = isGuest,
+ isCurrent = isCurrent,
+ isSwitchToEnabled = canSwitchUsers || (isCurrent && !isGuest),
+ )
+ }
+
+ @JvmStatic
+ fun createRecord(
+ context: Context,
+ selectedUserId: Int,
+ actionType: UserActionModel,
+ isRestricted: Boolean,
+ isSwitchToEnabled: Boolean,
+ ): UserRecord {
+ return UserRecord(
+ isGuest = actionType == UserActionModel.ENTER_GUEST_MODE,
+ isAddUser = actionType == UserActionModel.ADD_USER,
+ isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER,
+ isRestricted = isRestricted,
+ isSwitchToEnabled = isSwitchToEnabled,
+ enforcedAdmin =
+ getEnforcedAdmin(
+ context = context,
+ selectedUserId = selectedUserId,
+ ),
+ )
+ }
+
+ fun toUserActionModel(record: UserRecord): UserActionModel {
+ check(!isUser(record))
+
+ return when {
+ record.isAddUser -> UserActionModel.ADD_USER
+ record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
+ record.isGuest -> UserActionModel.ENTER_GUEST_MODE
+ else -> error("Not a known action: $record")
+ }
+ }
+
+ fun isUser(record: UserRecord): Boolean {
+ return record.info != null
+ }
+
+ private fun getEnforcedAdmin(
+ context: Context,
+ selectedUserId: Int,
+ ): EnforcedAdmin? {
+ val admin =
+ RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
+ context,
+ UserManager.DISALLOW_ADD_USER,
+ selectedUserId,
+ )
+ ?: return null
+
+ return if (
+ !RestrictedLockUtilsInternal.hasBaseUserRestriction(
+ context,
+ UserManager.DISALLOW_ADD_USER,
+ selectedUserId,
+ )
+ ) {
+ admin
+ } else {
+ null
+ }
+ }
+
+ private fun getPicture(
+ context: Context,
+ manager: UserManager,
+ userInfo: UserInfo,
+ picture: Bitmap?,
+ ): Bitmap? {
+ if (userInfo.isGuest) {
+ return null
+ }
+
+ if (picture != null) {
+ return picture
+ }
+
+ val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null
+
+ val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size)
+ return Bitmap.createScaledBitmap(
+ unscaledOrNull,
+ avatarSize,
+ avatarSize,
+ /* filter= */ true,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
index bf7977a600e9..2095683ccb4c 100644
--- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
@@ -32,4 +32,6 @@ data class UserModel(
val isSelected: Boolean,
/** Whether this use is selectable. A non-selectable user cannot be switched to. */
val isSelectable: Boolean,
+ /** Whether this model represents the guest user. */
+ val isGuest: Boolean,
)
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
new file mode 100644
index 000000000000..a9d66de118e0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.systemui.user.ui.dialog
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.UserHandle
+import com.android.settingslib.R
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.user.CreateUserActivity
+
+/** Dialog for adding a new user to the device. */
+class AddUserDialog(
+ context: Context,
+ userHandle: UserHandle,
+ isKeyguardShowing: Boolean,
+ showEphemeralMessage: Boolean,
+ private val falsingManager: FalsingManager,
+ private val broadcastSender: BroadcastSender,
+ private val dialogLaunchAnimator: DialogLaunchAnimator
+) : SystemUIDialog(context) {
+
+ private val onClickListener =
+ object : DialogInterface.OnClickListener {
+ override fun onClick(dialog: DialogInterface, which: Int) {
+ val penalty =
+ if (which == BUTTON_NEGATIVE) {
+ FalsingManager.NO_PENALTY
+ } else {
+ FalsingManager.MODERATE_PENALTY
+ }
+ if (falsingManager.isFalseTap(penalty)) {
+ return
+ }
+
+ if (which == BUTTON_NEUTRAL) {
+ cancel()
+ return
+ }
+
+ dialogLaunchAnimator.dismissStack(this@AddUserDialog)
+ if (ActivityManager.isUserAMonkey()) {
+ return
+ }
+
+ // Use broadcast instead of ShadeController, as this dialog may have started in
+ // another
+ // process where normal dagger bindings are not available.
+ broadcastSender.sendBroadcastAsUser(
+ Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
+ UserHandle.CURRENT
+ )
+
+ context.startActivityAsUser(
+ CreateUserActivity.createIntentForStart(context),
+ userHandle,
+ )
+ }
+ }
+
+ init {
+ setTitle(R.string.user_add_user_title)
+ val message =
+ context.getString(R.string.user_add_user_message_short) +
+ if (showEphemeralMessage) {
+ context.getString(
+ com.android.systemui.R.string.user_add_user_message_guest_remove
+ )
+ } else {
+ ""
+ }
+ setMessage(message)
+
+ setButton(
+ BUTTON_NEUTRAL,
+ context.getString(android.R.string.cancel),
+ onClickListener,
+ )
+
+ setButton(
+ BUTTON_POSITIVE,
+ context.getString(android.R.string.ok),
+ onClickListener,
+ )
+
+ setWindowOnTop(this, isKeyguardShowing)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt
new file mode 100644
index 000000000000..19ad44d8649f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.systemui.user.ui.dialog
+
+import android.annotation.UserIdInt
+import android.content.Context
+import android.content.DialogInterface
+import com.android.settingslib.R
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** Dialog for exiting the guest user. */
+class ExitGuestDialog(
+ context: Context,
+ private val guestUserId: Int,
+ private val isGuestEphemeral: Boolean,
+ private val targetUserId: Int,
+ isKeyguardShowing: Boolean,
+ private val falsingManager: FalsingManager,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val onExitGuestUserListener: OnExitGuestUserListener,
+) : SystemUIDialog(context) {
+
+ fun interface OnExitGuestUserListener {
+ fun onExitGuestUser(
+ @UserIdInt guestId: Int,
+ @UserIdInt targetId: Int,
+ forceRemoveGuest: Boolean,
+ )
+ }
+
+ private val onClickListener =
+ object : DialogInterface.OnClickListener {
+ override fun onClick(dialog: DialogInterface, which: Int) {
+ val penalty =
+ if (which == BUTTON_NEGATIVE) {
+ FalsingManager.NO_PENALTY
+ } else {
+ FalsingManager.MODERATE_PENALTY
+ }
+ if (falsingManager.isFalseTap(penalty)) {
+ return
+ }
+
+ if (isGuestEphemeral) {
+ if (which == BUTTON_POSITIVE) {
+ dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+ // Ephemeral guest: exit guest, guest is removed by the system
+ // on exit, since its marked ephemeral
+ onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, false)
+ } else if (which == BUTTON_NEGATIVE) {
+ // Cancel clicked, do nothing
+ cancel()
+ }
+ } else {
+ when (which) {
+ BUTTON_POSITIVE -> {
+ dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+ // Non-ephemeral guest: exit guest, guest is not removed by the system
+ // on exit, since its marked non-ephemeral
+ onExitGuestUserListener.onExitGuestUser(
+ guestUserId,
+ targetUserId,
+ false
+ )
+ }
+ BUTTON_NEGATIVE -> {
+ dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+ // Non-ephemeral guest: remove guest and then exit
+ onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, true)
+ }
+ BUTTON_NEUTRAL -> {
+ // Cancel clicked, do nothing
+ cancel()
+ }
+ }
+ }
+ }
+ }
+
+ init {
+ if (isGuestEphemeral) {
+ setTitle(context.getString(R.string.guest_exit_dialog_title))
+ setMessage(context.getString(R.string.guest_exit_dialog_message))
+ setButton(
+ BUTTON_NEUTRAL,
+ context.getString(android.R.string.cancel),
+ onClickListener,
+ )
+ setButton(
+ BUTTON_POSITIVE,
+ context.getString(R.string.guest_exit_dialog_button),
+ onClickListener,
+ )
+ } else {
+ setTitle(context.getString(R.string.guest_exit_dialog_title_non_ephemeral))
+ setMessage(context.getString(R.string.guest_exit_dialog_message_non_ephemeral))
+ setButton(
+ BUTTON_NEUTRAL,
+ context.getString(android.R.string.cancel),
+ onClickListener,
+ )
+ setButton(
+ BUTTON_NEGATIVE,
+ context.getString(R.string.guest_exit_clear_data_button),
+ onClickListener,
+ )
+ setButton(
+ BUTTON_POSITIVE,
+ context.getString(R.string.guest_exit_save_data_button),
+ onClickListener,
+ )
+ }
+ setWindowOnTop(this, isKeyguardShowing)
+ setCanceledOnTouchOutside(false)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt
new file mode 100644
index 000000000000..c1d2f4788147
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.systemui.user.ui.dialog
+
+import com.android.systemui.CoreStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface UserDialogModule {
+
+ @Binds
+ @IntoMap
+ @ClassKey(UserSwitcherDialogCoordinator::class)
+ fun bindFeature(impl: UserSwitcherDialogCoordinator): CoreStartable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
new file mode 100644
index 000000000000..6e7b5232d818
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.systemui.user.ui.dialog
+
+import android.app.Dialog
+import android.content.Context
+import com.android.settingslib.users.UserCreatingDialog
+import com.android.systemui.CoreStartable
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+/** Coordinates dialogs for user switcher logic. */
+@SysUISingleton
+class UserSwitcherDialogCoordinator
+@Inject
+constructor(
+ @Application private val context: Context,
+ @Application private val applicationScope: CoroutineScope,
+ private val falsingManager: FalsingManager,
+ private val broadcastSender: BroadcastSender,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val interactor: UserInteractor,
+ private val featureFlags: FeatureFlags,
+) : CoreStartable(context) {
+
+ private var currentDialog: Dialog? = null
+
+ override fun start() {
+ if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
+ return
+ }
+
+ startHandlingDialogShowRequests()
+ startHandlingDialogDismissRequests()
+ }
+
+ private fun startHandlingDialogShowRequests() {
+ applicationScope.launch {
+ interactor.dialogShowRequests.filterNotNull().collect { request ->
+ currentDialog?.let {
+ if (it.isShowing) {
+ it.cancel()
+ }
+ }
+
+ currentDialog =
+ when (request) {
+ is ShowDialogRequestModel.ShowAddUserDialog ->
+ AddUserDialog(
+ context = context,
+ userHandle = request.userHandle,
+ isKeyguardShowing = request.isKeyguardShowing,
+ showEphemeralMessage = request.showEphemeralMessage,
+ falsingManager = falsingManager,
+ broadcastSender = broadcastSender,
+ dialogLaunchAnimator = dialogLaunchAnimator,
+ )
+ is ShowDialogRequestModel.ShowUserCreationDialog ->
+ UserCreatingDialog(
+ context,
+ request.isGuest,
+ )
+ is ShowDialogRequestModel.ShowExitGuestDialog ->
+ ExitGuestDialog(
+ context = context,
+ guestUserId = request.guestUserId,
+ isGuestEphemeral = request.isGuestEphemeral,
+ targetUserId = request.targetUserId,
+ isKeyguardShowing = request.isKeyguardShowing,
+ falsingManager = falsingManager,
+ dialogLaunchAnimator = dialogLaunchAnimator,
+ onExitGuestUserListener = request.onExitGuestUser,
+ )
+ }
+
+ currentDialog?.show()
+ interactor.onDialogShown()
+ }
+ }
+ }
+
+ private fun startHandlingDialogDismissRequests() {
+ applicationScope.launch {
+ interactor.dialogDismissRequests.filterNotNull().collect {
+ currentDialog?.let {
+ if (it.isShowing) {
+ it.cancel()
+ }
+ }
+
+ interactor.onDialogDismissed()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 398341d256d2..5b83df7b4a36 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -21,7 +21,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.android.systemui.R
import com.android.systemui.common.ui.drawable.CircularDrawable
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
@@ -36,9 +39,14 @@ import kotlinx.coroutines.flow.map
class UserSwitcherViewModel
private constructor(
private val userInteractor: UserInteractor,
+ private val guestUserInteractor: GuestUserInteractor,
private val powerInteractor: PowerInteractor,
+ private val featureFlags: FeatureFlags,
) : ViewModel() {
+ private val isNewImpl: Boolean
+ get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
/** On-device users. */
val users: Flow<List<UserViewModel>> =
userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
@@ -47,9 +55,6 @@ private constructor(
val maximumUserColumns: Flow<Int> =
users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
- /** Whether the button to open the user action menu is visible. */
- val isOpenMenuButtonVisible: Flow<Boolean> = userInteractor.actions.map { it.isNotEmpty() }
-
private val _isMenuVisible = MutableStateFlow(false)
/**
* Whether the user action menu should be shown. Once the action menu is dismissed/closed, the
@@ -58,9 +63,23 @@ private constructor(
val isMenuVisible: Flow<Boolean> = _isMenuVisible
/** The user action menu. */
val menu: Flow<List<UserActionViewModel>> =
- userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
+ userInteractor.actions.map { actions ->
+ if (isNewImpl && actions.isNotEmpty()) {
+ // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
+ // switcher specific action that is not known to the our data source or other
+ // features.
+ actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+ } else {
+ actions
+ }
+ .map { action -> toViewModel(action) }
+ }
+
+ /** Whether the button to open the user action menu is visible. */
+ val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
private val hasCancelButtonBeenClicked = MutableStateFlow(false)
+ private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false)
/**
* Whether the observer should finish the experience. Once consumed, [onFinished] must be called
@@ -81,6 +100,7 @@ private constructor(
*/
fun onFinished() {
hasCancelButtonBeenClicked.value = false
+ isFinishRequiredDueToExecutedAction.value = false
}
/** Notifies that the user has clicked the "open menu" button. */
@@ -120,8 +140,10 @@ private constructor(
},
// When the cancel button is clicked, we should finish.
hasCancelButtonBeenClicked,
- ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked ->
- selectedUserChanged || screenTurnedOff || cancelButtonClicked
+ // If an executed action told us to finish, we should finish,
+ isFinishRequiredDueToExecutedAction,
+ ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish ->
+ selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish
}
}
@@ -164,13 +186,25 @@ private constructor(
} else {
LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
isGuest = model == UserActionModel.ENTER_GUEST_MODE,
- isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
- isGuestUserResetting = userInteractor.isGuestUserResetting,
+ isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
+ isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
isAddUser = model == UserActionModel.ADD_USER,
)
},
- onClicked = { userInteractor.executeAction(action = model) },
+ onClicked = {
+ userInteractor.executeAction(action = model)
+ // We don't finish because we want to show a dialog over the full-screen UI and
+ // that dialog can be dismissed in case the user changes their mind and decides not
+ // to add a user.
+ //
+ // We finish for all other actions because they navigate us away from the
+ // full-screen experience or are destructive (like changing to the guest user).
+ val shouldFinish = model != UserActionModel.ADD_USER
+ if (shouldFinish) {
+ isFinishRequiredDueToExecutedAction.value = true
+ }
+ },
)
}
@@ -186,13 +220,17 @@ private constructor(
@Inject
constructor(
private val userInteractor: UserInteractor,
+ private val guestUserInteractor: GuestUserInteractor,
private val powerInteractor: PowerInteractor,
+ private val featureFlags: FeatureFlags,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return UserSwitcherViewModel(
userInteractor = userInteractor,
+ guestUserInteractor = guestUserInteractor,
powerInteractor = powerInteractor,
+ featureFlags = featureFlags,
)
as T
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
new file mode 100644
index 000000000000..0b8257da8fb5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.systemui.util.settings
+
+import android.annotation.UserIdInt
+import android.database.ContentObserver
+import android.os.UserHandle
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Kotlin extension functions for [SettingsProxy]. */
+object SettingsProxyExt {
+
+ /** Returns a flow of [Unit] that is invoked each time that content is updated. */
+ fun SettingsProxy.observerFlow(
+ vararg names: String,
+ @UserIdInt userId: Int = UserHandle.USER_CURRENT,
+ ): Flow<Unit> {
+ return conflatedCallbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ names.forEach { name -> registerContentObserverForUser(name, observer, userId) }
+
+ awaitClose { unregisterContentObserver(observer) }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 43f6f1aac097..c1036e356cfa 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -411,7 +411,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase {
0 /* flags */);
users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
- false /* isAddSupervisedUser */));
+ false /* isAddSupervisedUser */, null /* enforcedAdmin */));
}
return users;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index ba1e168bc316..eea2e952c81f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -116,6 +116,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this)
assertThat(latest).isFalse()
+ assertThat(underTest.isKeyguardShowing()).isFalse()
val captor = argumentCaptor<KeyguardStateController.Callback>()
verify(keyguardStateController).addCallback(captor.capture())
@@ -123,10 +124,12 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
whenever(keyguardStateController.isShowing).thenReturn(true)
captor.value.onKeyguardShowingChanged()
assertThat(latest).isTrue()
+ assertThat(underTest.isKeyguardShowing()).isTrue()
whenever(keyguardStateController.isShowing).thenReturn(false)
captor.value.onKeyguardShowingChanged()
assertThat(latest).isFalse()
+ assertThat(underTest.isKeyguardShowing()).isFalse()
job.cancel()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
index ff0faf98fe1e..098086a61cd0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
@@ -35,7 +35,9 @@ import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.statusbar.IUndoMediaTransferCallback
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.FakeExecutor
@@ -43,10 +45,12 @@ import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
@@ -75,6 +79,14 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
private lateinit var windowManager: WindowManager
@Mock
private lateinit var commandQueue: CommandQueue
+ @Mock
+ private lateinit var lazyFalsingManager: Lazy<FalsingManager>
+ @Mock
+ private lateinit var falsingManager: FalsingManager
+ @Mock
+ private lateinit var lazyFalsingCollector: Lazy<FalsingCollector>
+ @Mock
+ private lateinit var falsingCollector: FalsingCollector
private lateinit var commandQueueCallback: CommandQueue.Callbacks
private lateinit var fakeAppIconDrawable: Drawable
private lateinit var fakeClock: FakeSystemClock
@@ -101,6 +113,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
+ whenever(lazyFalsingManager.get()).thenReturn(falsingManager)
+ whenever(lazyFalsingCollector.get()).thenReturn(falsingCollector)
controllerSender = MediaTttChipControllerSender(
commandQueue,
@@ -111,7 +125,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
accessibilityManager,
configurationController,
powerManager,
- senderUiEventLogger
+ senderUiEventLogger,
+ lazyFalsingManager,
+ lazyFalsingCollector
)
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
@@ -417,6 +433,38 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
}
@Test
+ fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() {
+ whenever(lazyFalsingManager.get().isFalseTap(anyInt())).thenReturn(true)
+ var undoCallbackCalled = false
+ val undoCallback = object : IUndoMediaTransferCallback.Stub() {
+ override fun onUndoTriggered() {
+ undoCallbackCalled = true
+ }
+ }
+
+ controllerSender.displayView(transferToReceiverSucceeded(undoCallback))
+ getChipView().getUndoButton().performClick()
+
+ assertThat(undoCallbackCalled).isFalse()
+ }
+
+ @Test
+ fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() {
+ whenever(lazyFalsingManager.get().isFalseTap(anyInt())).thenReturn(false)
+ var undoCallbackCalled = false
+ val undoCallback = object : IUndoMediaTransferCallback.Stub() {
+ override fun onUndoTriggered() {
+ undoCallbackCalled = true
+ }
+ }
+
+ controllerSender.displayView(transferToReceiverSucceeded(undoCallback))
+ getChipView().getUndoButton().performClick()
+
+ assertThat(undoCallbackCalled).isTrue()
+ }
+
+ @Test
fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
val undoCallback = object : IUndoMediaTransferCallback.Stub() {
override fun onUndoTriggered() {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 5d5918de3d9e..d2c2d58820bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -14,6 +14,9 @@
package com.android.systemui.qs;
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertTrue;
@@ -49,13 +52,13 @@ import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.media.MediaHost;
import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.dagger.QSFragmentComponent;
import com.android.systemui.qs.external.TileServiceRequestController;
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
@@ -93,7 +96,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
@Mock private QSPanel.QSTileLayout mQsTileLayout;
@Mock private QSPanel.QSTileLayout mQQsTileLayout;
@Mock private QSAnimator mQSAnimator;
- @Mock private StatusBarStateController mStatusBarStateController;
+ @Mock private SysuiStatusBarStateController mStatusBarStateController;
@Mock private QSSquishinessController mSquishinessController;
private View mQsFragmentView;
@@ -158,7 +161,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
public void
transitionToFullShade_onKeyguard_noBouncer_setsAlphaUsingLinearInterpolator() {
QSFragment fragment = resumeAndGetFragment();
- setStatusBarState(StatusBarState.KEYGUARD);
+ setStatusBarState(KEYGUARD);
when(mQSPanelController.isBouncerInTransit()).thenReturn(false);
boolean isTransitioningToFullShade = true;
float transitionProgress = 0.5f;
@@ -174,7 +177,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
public void
transitionToFullShade_onKeyguard_bouncerActive_setsAlphaUsingBouncerInterpolator() {
QSFragment fragment = resumeAndGetFragment();
- setStatusBarState(StatusBarState.KEYGUARD);
+ setStatusBarState(KEYGUARD);
when(mQSPanelController.isBouncerInTransit()).thenReturn(true);
boolean isTransitioningToFullShade = true;
float transitionProgress = 0.5f;
@@ -262,6 +265,27 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
}
@Test
+ public void setQsExpansion_inSplitShade_whenTransitioningToKeyguard_setsAlphaBasedOnShadeTransitionProgress() {
+ QSFragment fragment = resumeAndGetFragment();
+ enableSplitShade();
+ when(mStatusBarStateController.getState()).thenReturn(SHADE);
+ when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
+ boolean isTransitioningToFullShade = false;
+ float transitionProgress = 0;
+ float squishinessFraction = 0f;
+
+ fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+ squishinessFraction);
+
+ // trigger alpha refresh with non-zero expansion and fraction values
+ fragment.setQsExpansion(/* expansion= */ 1, /* panelExpansionFraction= */1,
+ /* proposedTranslation= */ 0, /* squishinessFraction= */ 1);
+
+ // alpha should follow lockscreen to shade progress, not panel expansion fraction
+ assertThat(mQsFragmentView.getAlpha()).isEqualTo(transitionProgress);
+ }
+
+ @Test
public void getQsMinExpansionHeight_notInSplitShade_returnsHeaderHeight() {
QSFragment fragment = resumeAndGetFragment();
disableSplitShade();
@@ -402,6 +426,19 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
verify(mQSPanelController).setListening(eq(true), anyBoolean());
}
+ @Test
+ public void passCorrectExpansionState_inSplitShade() {
+ QSFragment fragment = resumeAndGetFragment();
+ enableSplitShade();
+ clearInvocations(mQSPanelController);
+
+ fragment.setExpanded(true);
+ verify(mQSPanelController).setExpanded(true);
+
+ fragment.setExpanded(false);
+ verify(mQSPanelController).setExpanded(false);
+ }
+
@Override
protected Fragment instantiate(Context context, String className, Bundle arguments) {
MockitoAnnotations.initMocks(this);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
index 7d563399ee1c..4c44dacab1a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
@@ -33,6 +33,7 @@ import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
import android.provider.MediaStore;
import android.testing.AndroidTestingRunner;
@@ -97,7 +98,8 @@ public class ImageExporterTest extends SysuiTestCase {
Bitmap original = createCheckerBitmap(10, 10, 10);
ListenableFuture<ImageExporter.Result> direct =
- exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME);
+ exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME,
+ Process.myUserHandle());
assertTrue("future should be done", direct.isDone());
assertFalse("future should not be canceled", direct.isCancelled());
ImageExporter.Result result = direct.get();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
index 69b7b88b0524..8c9404e336ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
@@ -180,7 +180,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task =
- new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+ new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
ActionTransition::new, mSmartActionsProvider);
Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(),
@@ -208,7 +208,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task =
- new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+ new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
ActionTransition::new, mSmartActionsProvider);
Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(),
@@ -236,7 +236,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task =
- new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+ new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
ActionTransition::new, mSmartActionsProvider);
Notification.Action deleteAction = task.createDeleteAction(mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index b40d5ac69d7b..0c60d3c72c2a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -23,6 +23,9 @@ import static com.android.keyguard.KeyguardClockSwitch.SMALL;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
+import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED;
+import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN;
+import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING;
import static com.google.common.truth.Truth.assertThat;
@@ -1249,14 +1252,10 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
@Test
public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() {
enableSplitShade(/* enabled= */ true);
- // set panel state to CLOSED
- mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0,
- /* expanded= */ false, /* tracking= */ false, /* dragDownPxAmount= */ 0);
+ mPanelExpansionStateManager.updateState(STATE_CLOSED);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
- // change panel state to OPENING
- mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0.5f,
- /* expanded= */ true, /* tracking= */ true, /* dragDownPxAmount= */ 100);
+ mPanelExpansionStateManager.updateState(STATE_OPENING);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isTrue();
}
@@ -1264,15 +1263,23 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
@Test
public void testQsNotToBeImmediatelyExpandedWhenGoingFromUnlockedToLocked() {
enableSplitShade(/* enabled= */ true);
- // set panel state to CLOSED
- mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0,
- /* expanded= */ false, /* tracking= */ false, /* dragDownPxAmount= */ 0);
+ mPanelExpansionStateManager.updateState(STATE_CLOSED);
- // go to lockscreen, which also sets fraction to 1.0f and makes shade "expanded"
mStatusBarStateController.setState(KEYGUARD);
- mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
- /* expanded= */ true, /* tracking= */ true, /* dragDownPxAmount= */ 0);
+ // going to lockscreen would trigger STATE_OPENING
+ mPanelExpansionStateManager.updateState(STATE_OPENING);
+
+ assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
+ }
+
+ @Test
+ public void testQsImmediateResetsWhenPanelOpensOrCloses() {
+ mNotificationPanelViewController.mQsExpandImmediate = true;
+ mPanelExpansionStateManager.updateState(STATE_OPEN);
+ assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
+ mNotificationPanelViewController.mQsExpandImmediate = true;
+ mPanelExpansionStateManager.updateState(STATE_CLOSED);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
deleted file mode 100644
index eb4cca822c7f..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar;
-
-import static org.junit.Assert.assertFalse;
-
-import android.os.Handler;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.Dependency;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Verifies that particular sets of dependencies don't have dependencies on others. For example,
- * code managing notifications shouldn't directly depend on CentralSurfaces, since there are
- * platforms which want to manage notifications, but don't use CentralSurfaces.
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class NonPhoneDependencyTest extends SysuiTestCase {
- @Mock private NotificationPresenter mPresenter;
- @Mock private NotificationListContainer mListContainer;
- @Mock private RemoteInputController.Delegate mDelegate;
- @Mock private NotificationRemoteInputManager.Callback mRemoteInputManagerCallback;
- @Mock private OnSettingsClickListener mOnSettingsClickListener;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
- mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
- new Handler(TestableLooper.get(this).getLooper()));
- }
-
- @Ignore("Causes binder calls which fail")
- @Test
- public void testNotificationManagementCodeHasNoDependencyOnStatusBarWindowManager() {
- NotificationGutsManager gutsManager = Dependency.get(NotificationGutsManager.class);
- NotificationLogger notificationLogger = Dependency.get(NotificationLogger.class);
- NotificationMediaManager mediaManager = Dependency.get(NotificationMediaManager.class);
- NotificationRemoteInputManager remoteInputManager =
- Dependency.get(NotificationRemoteInputManager.class);
- NotificationLockscreenUserManager lockscreenUserManager =
- Dependency.get(NotificationLockscreenUserManager.class);
- gutsManager.setUpWithPresenter(mPresenter, mListContainer,
- mOnSettingsClickListener);
- notificationLogger.setUpWithContainer(mListContainer);
- mediaManager.setUpWithPresenter(mPresenter);
- remoteInputManager.setUpWithCallback(mRemoteInputManagerCallback,
- mDelegate);
- lockscreenUserManager.setUpWithPresenter(mPresenter);
-
- TestableLooper.get(this).processAllMessages();
- assertFalse(mDependency.hasInstantiatedDependency(NotificationShadeWindowController.class));
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 853d1df4b9bc..bdafa4893c9e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -52,6 +52,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -88,6 +89,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
@Mock
private NotificationClickNotifier mClickNotifier;
@Mock
+ private OverviewProxyService mOverviewProxyService;
+ @Mock
private KeyguardManager mKeyguardManager;
@Mock
private DeviceProvisionedController mDeviceProvisionedController;
@@ -344,6 +347,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
(() -> mVisibilityProvider),
(() -> mNotifCollection),
mClickNotifier,
+ (() -> mOverviewProxyService),
NotificationLockscreenUserManagerTest.this.mKeyguardManager,
mStatusBarStateController,
Handler.createAsync(Looper.myLooper()),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 2ee31265ff7b..2970807afb36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -337,6 +337,40 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
}
@Test
+ fun testOnEntryUpdated_toAlert() {
+ // GIVEN that an entry is posted that should not heads up
+ setShouldHeadsUp(mEntry, false)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // WHEN it's updated to heads up
+ setShouldHeadsUp(mEntry)
+ mCollectionListener.onEntryUpdated(mEntry)
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification alerts
+ finishBind(mEntry)
+ verify(mHeadsUpManager).showNotification(mEntry)
+ }
+
+ @Test
+ fun testOnEntryUpdated_toNotAlert() {
+ // GIVEN that an entry is posted that should heads up
+ setShouldHeadsUp(mEntry)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // WHEN it's updated to not heads up
+ setShouldHeadsUp(mEntry, false)
+ mCollectionListener.onEntryUpdated(mEntry)
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification is never bound or shown
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+ }
+
+ @Test
fun testOnEntryRemovedRemovesHeadsUpNotification() {
// GIVEN the current HUN is mEntry
addHUN(mEntry)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
new file mode 100644
index 000000000000..f4d61c8e2a46
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
@@ -0,0 +1,323 @@
+/*
+ *
+ * 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.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import android.widget.RemoteViews
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationMemoryMonitorTest : SysuiTestCase() {
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_plainNotification() {
+ val notification = createBasicNotification().build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 3316,
+ bigPicture = 0,
+ extender = 0,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() {
+ val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
+ val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = 0,
+ extras = 3316,
+ bigPicture = 0,
+ extender = 0,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_customViewNotification_marksTrue() {
+ val notification =
+ createBasicNotification()
+ .setCustomContentView(
+ RemoteViews(context.packageName, android.R.layout.list_content)
+ )
+ .build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 3384,
+ bigPicture = 0,
+ extender = 0,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = true,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() {
+ val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444)
+ val notification =
+ createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = 444444,
+ largeIcon = 0,
+ extras = 3212,
+ bigPicture = 0,
+ extender = 0,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_bigPictureStyle() {
+ val bigPicture =
+ Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888))
+ val bigPictureIcon =
+ Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+ val notification =
+ createBasicNotification()
+ .setStyle(
+ Notification.BigPictureStyle()
+ .bigPicture(bigPicture)
+ .bigLargeIcon(bigPictureIcon)
+ )
+ .build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 4092,
+ bigPicture = 960000,
+ extender = 0,
+ style = "BigPictureStyle",
+ styleIcon = bigPictureIcon.bitmap.allocationByteCount,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_callingStyle() {
+ val personIcon =
+ Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+ val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+ val fakeIntent =
+ PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+ val notification =
+ createBasicNotification()
+ .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent))
+ .build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 4084,
+ bigPicture = 0,
+ extender = 0,
+ style = "CallStyle",
+ styleIcon = personIcon.bitmap.allocationByteCount,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_messagingStyle() {
+ val personIcon =
+ Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+ val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+ val message = Notification.MessagingStyle.Message("Message!", 4323, person)
+ val historicPersonIcon =
+ Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888))
+ val historicPerson =
+ Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build()
+ val historicMessage =
+ Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson)
+
+ val notification =
+ createBasicNotification()
+ .setStyle(
+ Notification.MessagingStyle(person)
+ .addMessage(message)
+ .addHistoricMessage(historicMessage)
+ )
+ .build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 5024,
+ bigPicture = 0,
+ extender = 0,
+ style = "MessagingStyle",
+ styleIcon =
+ personIcon.bitmap.allocationByteCount +
+ historicPersonIcon.bitmap.allocationByteCount,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_carExtender() {
+ val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888)
+ val extender = Notification.CarExtender().setLargeIcon(carIcon)
+ val notification = createBasicNotification().extend(extender).build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 3612,
+ bigPicture = 0,
+ extender = 556656,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = false,
+ )
+ }
+
+ @Test
+ fun currentNotificationMemoryUse_tvWearExtender() {
+ val tvExtender = Notification.TvExtender().setChannel("channel2")
+ val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888)
+ val wearExtender = Notification.WearableExtender().setBackground(wearBackground)
+ val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build()
+ val nmm = createNMMWithNotifications(listOf(notification))
+ val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+ assertNotificationObjectSizes(
+ memoryUse = memoryUse,
+ smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+ largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+ extras = 3820,
+ bigPicture = 0,
+ extender = 388 + wearBackground.allocationByteCount,
+ style = null,
+ styleIcon = 0,
+ hasCustomView = false,
+ )
+ }
+
+ private fun createBasicNotification(): Notification.Builder {
+ val smallIcon =
+ Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888))
+ val largeIcon =
+ Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888))
+ return Notification.Builder(context)
+ .setSmallIcon(smallIcon)
+ .setLargeIcon(largeIcon)
+ .setContentTitle("This is a title")
+ .setContentText("This is content text.")
+ }
+
+ /** This will generate a nicer error message than comparing objects */
+ private fun assertNotificationObjectSizes(
+ memoryUse: NotificationMemoryUsage,
+ smallIcon: Int,
+ largeIcon: Int,
+ extras: Int,
+ bigPicture: Int,
+ extender: Int,
+ style: String?,
+ styleIcon: Int,
+ hasCustomView: Boolean
+ ) {
+ assertThat(memoryUse.packageName).isEqualTo("test_pkg")
+ assertThat(memoryUse.notificationId)
+ .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0"))
+ assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon)
+ assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon)
+ assertThat(memoryUse.objectUsage.extras).isEqualTo(extras)
+ assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture)
+ assertThat(memoryUse.objectUsage.extender).isEqualTo(extender)
+ if (style == null) {
+ assertThat(memoryUse.objectUsage.style).isNull()
+ } else {
+ assertThat(memoryUse.objectUsage.style).isEqualTo(style)
+ }
+ assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon)
+ assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView)
+ }
+
+ private fun getUseObject(
+ singleItemUseList: List<NotificationMemoryUsage>
+ ): NotificationMemoryUsage {
+ assertThat(singleItemUseList).hasSize(1)
+ return singleItemUseList[0]
+ }
+
+ private fun createNMMWithNotifications(
+ notifications: List<Notification>
+ ): NotificationMemoryMonitor {
+ val notifPipeline: NotifPipeline = mock()
+ val notificationEntries =
+ notifications.map { n ->
+ NotificationEntryBuilder().setTag("test").setNotification(n).build()
+ }
+ whenever(notifPipeline.allNotifs).thenReturn(notificationEntries)
+ return NotificationMemoryMonitor(notifPipeline, mock())
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
index 7e97629e82e2..dae0aa229dbf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
@@ -40,6 +40,10 @@ public class NotificationPanelLoggerFake implements NotificationPanelLogger {
NotificationPanelLogger.toNotificationProto(visibleNotifications)));
}
+ @Override
+ public void logNotificationDrag(NotificationEntry draggedNotification) {
+ }
+
public static class CallRecord {
public boolean isLockscreen;
public Notifications.NotificationList list;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
index 922e93d06efc..ed2afe753a5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
@@ -40,6 +40,8 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerFake;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import org.junit.Before;
@@ -63,6 +65,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
private NotificationMenuRowPlugin.MenuItem mMenuItem =
mock(NotificationMenuRowPlugin.MenuItem.class);
private ShadeController mShadeController = mock(ShadeController.class);
+ private NotificationPanelLogger mNotificationPanelLogger = mock(NotificationPanelLogger.class);
@Before
public void setUp() throws Exception {
@@ -82,7 +85,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem);
mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager,
- mShadeController);
+ mShadeController, mNotificationPanelLogger);
}
@Test
@@ -96,6 +99,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
mRow.doDragCallback(0, 0);
verify(controller).startDragAndDrop(mRow);
verify(mHeadsUpManager, times(1)).releaseAllImmediately();
+ verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any());
}
@Test
@@ -107,6 +111,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
verify(controller).startDragAndDrop(mRow);
verify(mShadeController).animateCollapsePanels(eq(0), eq(true),
eq(false), anyFloat());
+ verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any());
}
@Test
@@ -124,6 +129,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
// Verify that we never start the actual drag since there is no content
verify(mRow, never()).startDragAndDrop(any(), any(), any(), anyInt());
+ verify(mNotificationPanelLogger, never()).logNotificationDrag(any());
}
private ExpandableNotificationRowDragController createSpyController() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index cfaa4707ef76..6ec5cf82a81e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -47,6 +47,7 @@ import androidx.test.filters.SmallTest;
import com.android.keyguard.CarrierTextController;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.logging.KeyguardLogger;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.battery.BatteryMeterViewController;
@@ -123,6 +124,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase {
private StatusBarUserInfoTracker mStatusBarUserInfoTracker;
@Mock private SecureSettings mSecureSettings;
@Mock private CommandQueue mCommandQueue;
+ @Mock private KeyguardLogger mLogger;
private TestNotificationPanelViewStateProvider mNotificationPanelViewStateProvider;
private KeyguardStatusBarView mKeyguardStatusBarView;
@@ -172,7 +174,8 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase {
mStatusBarUserInfoTracker,
mSecureSettings,
mCommandQueue,
- mFakeExecutor
+ mFakeExecutor,
+ mLogger
);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
new file mode 100644
index 000000000000..773a0d8ceb64
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.systemui.telephony.data.repository
+
+import android.telephony.TelephonyCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.telephony.TelephonyListenerManager
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class TelephonyRepositoryImplTest : SysuiTestCase() {
+
+ @Mock private lateinit var manager: TelephonyListenerManager
+
+ private lateinit var underTest: TelephonyRepositoryImpl
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest =
+ TelephonyRepositoryImpl(
+ manager = manager,
+ )
+ }
+
+ @Test
+ fun callState() =
+ runBlocking(IMMEDIATE) {
+ var callState: Int? = null
+ val job = underTest.callState.onEach { callState = it }.launchIn(this)
+ val listenerCaptor = kotlinArgumentCaptor<TelephonyCallback.CallStateListener>()
+ verify(manager).addCallStateListener(listenerCaptor.capture())
+ val listener = listenerCaptor.value
+
+ listener.onCallStateChanged(0)
+ assertThat(callState).isEqualTo(0)
+
+ listener.onCallStateChanged(1)
+ assertThat(callState).isEqualTo(1)
+
+ listener.onCallStateChanged(2)
+ assertThat(callState).isEqualTo(2)
+
+ job.cancel()
+
+ verify(manager).removeCallStateListener(listener)
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
new file mode 100644
index 000000000000..4a8e0552d778
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.systemui.user.data.repository
+
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() {
+
+ @Before
+ fun setUp() {
+ super.setUp(isRefactored = true)
+ }
+
+ @Test
+ fun userSwitcherSettings() = runSelfCancelingTest {
+ setUpGlobalSettings(
+ isSimpleUserSwitcher = true,
+ isAddUsersFromLockscreen = true,
+ isUserSwitcherEnabled = true,
+ )
+ underTest = create(this)
+
+ var value: UserSwitcherSettingsModel? = null
+ underTest.userSwitcherSettings.onEach { value = it }.launchIn(this)
+
+ assertUserSwitcherSettings(
+ model = value,
+ expectedSimpleUserSwitcher = true,
+ expectedAddUsersFromLockscreen = true,
+ expectedUserSwitcherEnabled = true,
+ )
+
+ setUpGlobalSettings(
+ isSimpleUserSwitcher = false,
+ isAddUsersFromLockscreen = true,
+ isUserSwitcherEnabled = true,
+ )
+ assertUserSwitcherSettings(
+ model = value,
+ expectedSimpleUserSwitcher = false,
+ expectedAddUsersFromLockscreen = true,
+ expectedUserSwitcherEnabled = true,
+ )
+ }
+
+ @Test
+ fun refreshUsers() = runSelfCancelingTest {
+ underTest = create(this)
+ val initialExpectedValue =
+ setUpUsers(
+ count = 3,
+ selectedIndex = 0,
+ )
+ var userInfos: List<UserInfo>? = null
+ var selectedUserInfo: UserInfo? = null
+ underTest.userInfos.onEach { userInfos = it }.launchIn(this)
+ underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
+
+ underTest.refreshUsers()
+ assertThat(userInfos).isEqualTo(initialExpectedValue)
+ assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
+ assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
+
+ val secondExpectedValue =
+ setUpUsers(
+ count = 4,
+ selectedIndex = 1,
+ )
+ underTest.refreshUsers()
+ assertThat(userInfos).isEqualTo(secondExpectedValue)
+ assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
+ assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
+
+ val selectedNonGuestUserId = selectedUserInfo?.id
+ val thirdExpectedValue =
+ setUpUsers(
+ count = 2,
+ hasGuest = true,
+ selectedIndex = 1,
+ )
+ underTest.refreshUsers()
+ assertThat(userInfos).isEqualTo(thirdExpectedValue)
+ assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
+ assertThat(selectedUserInfo?.isGuest).isTrue()
+ assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
+ }
+
+ private fun setUpUsers(
+ count: Int,
+ hasGuest: Boolean = false,
+ selectedIndex: Int = 0,
+ ): List<UserInfo> {
+ val userInfos =
+ (0 until count).map { index ->
+ createUserInfo(
+ index,
+ isGuest = hasGuest && index == count - 1,
+ )
+ }
+ whenever(manager.aliveUsers).thenReturn(userInfos)
+ tracker.set(userInfos, selectedIndex)
+ return userInfos
+ }
+
+ private fun createUserInfo(
+ id: Int,
+ isGuest: Boolean,
+ ): UserInfo {
+ val flags = 0
+ return UserInfo(
+ id,
+ "user_$id",
+ /* iconPath= */ "",
+ flags,
+ if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags),
+ )
+ }
+
+ private fun setUpGlobalSettings(
+ isSimpleUserSwitcher: Boolean = false,
+ isAddUsersFromLockscreen: Boolean = false,
+ isUserSwitcherEnabled: Boolean = true,
+ ) {
+ context.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_expandLockScreenUserSwitcher,
+ true,
+ )
+ globalSettings.putIntForUser(
+ UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER,
+ if (isSimpleUserSwitcher) 1 else 0,
+ UserHandle.USER_SYSTEM,
+ )
+ globalSettings.putIntForUser(
+ Settings.Global.ADD_USERS_WHEN_LOCKED,
+ if (isAddUsersFromLockscreen) 1 else 0,
+ UserHandle.USER_SYSTEM,
+ )
+ globalSettings.putIntForUser(
+ Settings.Global.USER_SWITCHER_ENABLED,
+ if (isUserSwitcherEnabled) 1 else 0,
+ UserHandle.USER_SYSTEM,
+ )
+ }
+
+ private fun assertUserSwitcherSettings(
+ model: UserSwitcherSettingsModel?,
+ expectedSimpleUserSwitcher: Boolean,
+ expectedAddUsersFromLockscreen: Boolean,
+ expectedUserSwitcherEnabled: Boolean,
+ ) {
+ checkNotNull(model)
+ assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher)
+ assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen)
+ assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled)
+ }
+
+ /**
+ * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which
+ * is then automatically canceled and cleaned-up.
+ */
+ private fun runSelfCancelingTest(
+ block: suspend CoroutineScope.() -> Unit,
+ ) =
+ runBlocking(Dispatchers.Main.immediate) {
+ val scope = CoroutineScope(coroutineContext + Job())
+ block(scope)
+ scope.cancel()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 6fec343d036c..dcea83a55a74 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -17,201 +17,54 @@
package com.android.systemui.user.data.repository
-import android.content.pm.UserInfo
import android.os.UserManager
-import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
-import com.android.systemui.user.data.source.UserRecord
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.user.shared.model.UserModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.capture
-import com.google.common.truth.Truth.assertThat
+import com.android.systemui.util.settings.FakeSettings
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
+import kotlinx.coroutines.test.TestCoroutineScope
import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
-@SmallTest
-@RunWith(JUnit4::class)
-class UserRepositoryImplTest : SysuiTestCase() {
+abstract class UserRepositoryImplTest : SysuiTestCase() {
- @Mock private lateinit var manager: UserManager
- @Mock private lateinit var controller: UserSwitcherController
- @Captor
- private lateinit var userSwitchCallbackCaptor:
- ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
+ @Mock protected lateinit var manager: UserManager
+ @Mock protected lateinit var controller: UserSwitcherController
- private lateinit var underTest: UserRepositoryImpl
+ protected lateinit var underTest: UserRepositoryImpl
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
- whenever(controller.isGuestUserAutoCreated).thenReturn(false)
- whenever(controller.isGuestUserResetting).thenReturn(false)
-
- underTest =
- UserRepositoryImpl(
- appContext = context,
- manager = manager,
- controller = controller,
- )
- }
-
- @Test
- fun `users - registers for updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.users.onEach {}.launchIn(this)
-
- verify(controller).addUserSwitchCallback(any())
-
- job.cancel()
- }
-
- @Test
- fun `users - unregisters from updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.users.onEach {}.launchIn(this)
- verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-
- job.cancel()
-
- verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
- }
-
- @Test
- fun `users - does not include actions`() =
- runBlocking(IMMEDIATE) {
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0, isSelected = true),
- createActionRecord(UserActionModel.ADD_USER),
- createUserRecord(1),
- createUserRecord(2),
- createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
- createActionRecord(UserActionModel.ENTER_GUEST_MODE),
- )
- )
- var models: List<UserModel>? = null
- val job = underTest.users.onEach { models = it }.launchIn(this)
-
- assertThat(models).hasSize(3)
- assertThat(models?.get(0)?.id).isEqualTo(0)
- assertThat(models?.get(0)?.isSelected).isTrue()
- assertThat(models?.get(1)?.id).isEqualTo(1)
- assertThat(models?.get(1)?.isSelected).isFalse()
- assertThat(models?.get(2)?.id).isEqualTo(2)
- assertThat(models?.get(2)?.isSelected).isFalse()
- job.cancel()
- }
-
- @Test
- fun selectedUser() =
- runBlocking(IMMEDIATE) {
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0, isSelected = true),
- createUserRecord(1),
- createUserRecord(2),
- )
- )
- var id: Int? = null
- val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this)
+ protected lateinit var globalSettings: FakeSettings
+ protected lateinit var tracker: FakeUserTracker
+ protected lateinit var featureFlags: FakeFeatureFlags
- assertThat(id).isEqualTo(0)
-
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0),
- createUserRecord(1),
- createUserRecord(2, isSelected = true),
- )
- )
- verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
- userSwitchCallbackCaptor.value.onUserSwitched()
- assertThat(id).isEqualTo(2)
-
- job.cancel()
- }
-
- @Test
- fun `actions - unregisters from updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.actions.onEach {}.launchIn(this)
- verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-
- job.cancel()
-
- verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
- }
-
- @Test
- fun `actions - registers for updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.actions.onEach {}.launchIn(this)
-
- verify(controller).addUserSwitchCallback(any())
-
- job.cancel()
- }
-
- @Test
- fun `actopms - does not include users`() =
- runBlocking(IMMEDIATE) {
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0, isSelected = true),
- createActionRecord(UserActionModel.ADD_USER),
- createUserRecord(1),
- createUserRecord(2),
- createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
- createActionRecord(UserActionModel.ENTER_GUEST_MODE),
- )
- )
- var models: List<UserActionModel>? = null
- val job = underTest.actions.onEach { models = it }.launchIn(this)
-
- assertThat(models).hasSize(3)
- assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
- assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
- assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
- job.cancel()
- }
+ protected fun setUp(isRefactored: Boolean) {
+ MockitoAnnotations.initMocks(this)
- private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord {
- return UserRecord(
- info = UserInfo(id, "name$id", 0),
- isCurrent = isSelected,
- )
+ globalSettings = FakeSettings()
+ tracker = FakeUserTracker()
+ featureFlags = FakeFeatureFlags()
+ featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored)
}
- private fun createActionRecord(action: UserActionModel): UserRecord {
- return UserRecord(
- isAddUser = action == UserActionModel.ADD_USER,
- isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
- isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+ protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
+ return UserRepositoryImpl(
+ appContext = context,
+ manager = manager,
+ controller = controller,
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ globalSettings = globalSettings,
+ tracker = tracker,
+ featureFlags = featureFlags,
)
}
companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
+ @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
new file mode 100644
index 000000000000..d4b41c18e123
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
@@ -0,0 +1,205 @@
+/*
+ * 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.systemui.user.data.repository
+
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() {
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+
+ @Captor
+ private lateinit var userSwitchCallbackCaptor:
+ ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
+
+ @Before
+ fun setUp() {
+ super.setUp(isRefactored = false)
+
+ whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
+ whenever(controller.isGuestUserAutoCreated).thenReturn(false)
+ whenever(controller.isGuestUserResetting).thenReturn(false)
+
+ underTest = create()
+ }
+
+ @Test
+ fun `users - registers for updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.users.onEach {}.launchIn(this)
+
+ verify(controller).addUserSwitchCallback(any())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `users - unregisters from updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.users.onEach {}.launchIn(this)
+ verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+
+ job.cancel()
+
+ verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
+ }
+
+ @Test
+ fun `users - does not include actions`() =
+ runBlocking(IMMEDIATE) {
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0, isSelected = true),
+ createActionRecord(UserActionModel.ADD_USER),
+ createUserRecord(1),
+ createUserRecord(2),
+ createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
+ createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+ )
+ )
+ var models: List<UserModel>? = null
+ val job = underTest.users.onEach { models = it }.launchIn(this)
+
+ assertThat(models).hasSize(3)
+ assertThat(models?.get(0)?.id).isEqualTo(0)
+ assertThat(models?.get(0)?.isSelected).isTrue()
+ assertThat(models?.get(1)?.id).isEqualTo(1)
+ assertThat(models?.get(1)?.isSelected).isFalse()
+ assertThat(models?.get(2)?.id).isEqualTo(2)
+ assertThat(models?.get(2)?.isSelected).isFalse()
+ job.cancel()
+ }
+
+ @Test
+ fun selectedUser() =
+ runBlocking(IMMEDIATE) {
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0, isSelected = true),
+ createUserRecord(1),
+ createUserRecord(2),
+ )
+ )
+ var id: Int? = null
+ val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this)
+
+ assertThat(id).isEqualTo(0)
+
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0),
+ createUserRecord(1),
+ createUserRecord(2, isSelected = true),
+ )
+ )
+ verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+ userSwitchCallbackCaptor.value.onUserSwitched()
+ assertThat(id).isEqualTo(2)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - unregisters from updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.actions.onEach {}.launchIn(this)
+ verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+
+ job.cancel()
+
+ verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
+ }
+
+ @Test
+ fun `actions - registers for updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.actions.onEach {}.launchIn(this)
+
+ verify(controller).addUserSwitchCallback(any())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - does not include users`() =
+ runBlocking(IMMEDIATE) {
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0, isSelected = true),
+ createActionRecord(UserActionModel.ADD_USER),
+ createUserRecord(1),
+ createUserRecord(2),
+ createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
+ createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+ )
+ )
+ var models: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { models = it }.launchIn(this)
+
+ assertThat(models).hasSize(3)
+ assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
+ assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
+ assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
+ job.cancel()
+ }
+
+ private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord {
+ return UserRecord(
+ info = UserInfo(id, "name$id", 0),
+ isCurrent = isSelected,
+ )
+ }
+
+ private fun createActionRecord(action: UserActionModel): UserRecord {
+ return UserRecord(
+ isAddUser = action == UserActionModel.ADD_USER,
+ isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
+ isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
new file mode 100644
index 000000000000..120bf791c462
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
@@ -0,0 +1,394 @@
+/*
+ * 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.systemui.user.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScope
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class GuestUserInteractorTest : SysuiTestCase() {
+
+ @Mock private lateinit var manager: UserManager
+ @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var uiEventLogger: UiEventLogger
+ @Mock private lateinit var showDialog: (ShowDialogRequestModel) -> Unit
+ @Mock private lateinit var dismissDialog: () -> Unit
+ @Mock private lateinit var selectUser: (Int) -> Unit
+ @Mock private lateinit var switchUser: (Int) -> Unit
+
+ private lateinit var underTest: GuestUserInteractor
+
+ private lateinit var scope: TestCoroutineScope
+ private lateinit var repository: FakeUserRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(manager.createGuest(any())).thenReturn(GUEST_USER_INFO)
+
+ scope = TestCoroutineScope()
+ repository = FakeUserRepository()
+ repository.setUserInfos(ALL_USERS)
+
+ underTest =
+ GuestUserInteractor(
+ applicationContext = context,
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ manager = manager,
+ repository = repository,
+ deviceProvisionedController = deviceProvisionedController,
+ devicePolicyManager = devicePolicyManager,
+ refreshUsersScheduler =
+ RefreshUsersScheduler(
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ ),
+ uiEventLogger = uiEventLogger,
+ )
+ }
+
+ @Test
+ fun `onDeviceBootCompleted - allowed to add - create guest`() =
+ runBlocking(IMMEDIATE) {
+ setAllowedToAdd()
+
+ underTest.onDeviceBootCompleted()
+
+ verify(manager).createGuest(any())
+ verify(deviceProvisionedController, never()).addCallback(any())
+ }
+
+ @Test
+ fun `onDeviceBootCompleted - await provisioning - and create guest`() =
+ runBlocking(IMMEDIATE) {
+ setAllowedToAdd(isAllowed = false)
+ underTest.onDeviceBootCompleted()
+ val captor =
+ kotlinArgumentCaptor<DeviceProvisionedController.DeviceProvisionedListener>()
+ verify(deviceProvisionedController).addCallback(captor.capture())
+
+ setAllowedToAdd(isAllowed = true)
+ captor.value.onDeviceProvisionedChanged()
+
+ verify(manager).createGuest(any())
+ verify(deviceProvisionedController).removeCallback(captor.value)
+ }
+
+ @Test
+ fun createAndSwitchTo() =
+ runBlocking(IMMEDIATE) {
+ underTest.createAndSwitchTo(
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ selectUser = selectUser,
+ )
+
+ verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+ verify(manager).createGuest(any())
+ verify(dismissDialog).invoke()
+ verify(selectUser).invoke(GUEST_USER_INFO.id)
+ }
+
+ @Test
+ fun `createAndSwitchTo - fails to create - does not switch to`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.createGuest(any())).thenReturn(null)
+
+ underTest.createAndSwitchTo(
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ selectUser = selectUser,
+ )
+
+ verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+ verify(manager).createGuest(any())
+ verify(dismissDialog).invoke()
+ verify(selectUser, never()).invoke(anyInt())
+ }
+
+ @Test
+ fun `exit - returns to target user`() =
+ runBlocking(IMMEDIATE) {
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ val targetUserId = NON_GUEST_USER_INFO.id
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(manager, never()).removeUser(anyInt())
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `exit - returns to last non-guest`() =
+ runBlocking(IMMEDIATE) {
+ val expectedUserId = NON_GUEST_USER_INFO.id
+ whenever(manager.getUserInfo(expectedUserId)).thenReturn(NON_GUEST_USER_INFO)
+ repository.lastSelectedNonGuestUserId = expectedUserId
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = UserHandle.USER_NULL,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(manager, never()).removeUser(anyInt())
+ verify(switchUser).invoke(expectedUserId)
+ }
+
+ @Test
+ fun `exit - last non-guest was removed - returns to system`() =
+ runBlocking(IMMEDIATE) {
+ val removedUserId = 310
+ repository.lastSelectedNonGuestUserId = removedUserId
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = UserHandle.USER_NULL,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(manager, never()).removeUser(anyInt())
+ verify(switchUser).invoke(UserHandle.USER_SYSTEM)
+ }
+
+ @Test
+ fun `exit - guest was ephemeral - it is removed`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO))
+ repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO)
+ val targetUserId = NON_GUEST_USER_INFO.id
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id)
+ verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id)
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `exit - force remove guest - it is removed`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+ val targetUserId = NON_GUEST_USER_INFO.id
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = true,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
+ verify(manager).removeUser(GUEST_USER_INFO.id)
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `exit - selected different from guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = 123,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotExit()
+ }
+
+ @Test
+ fun `exit - selected is actually not a guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = NON_GUEST_USER_INFO.id,
+ targetUserId = 123,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotExit()
+ }
+
+ @Test
+ fun `remove - returns to target user`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ val targetUserId = NON_GUEST_USER_INFO.id
+ underTest.remove(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
+ verify(manager).removeUser(GUEST_USER_INFO.id)
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `remove - selected different from guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.remove(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = 123,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotRemove()
+ }
+
+ @Test
+ fun `remove - selected is actually not a guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.remove(
+ guestUserId = NON_GUEST_USER_INFO.id,
+ targetUserId = 123,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotRemove()
+ }
+
+ private fun setAllowedToAdd(isAllowed: Boolean = true) {
+ whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(isAllowed)
+ whenever(devicePolicyManager.isDeviceManaged).thenReturn(!isAllowed)
+ }
+
+ private fun verifyDidNotExit() {
+ verifyDidNotRemove()
+ verify(manager, never()).getUserInfo(anyInt())
+ verify(uiEventLogger, never()).log(any())
+ }
+
+ private fun verifyDidNotRemove() {
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(showDialog, never()).invoke(any())
+ verify(dismissDialog, never()).invoke()
+ verify(switchUser, never()).invoke(anyInt())
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ private val NON_GUEST_USER_INFO =
+ UserInfo(
+ /* id= */ 818,
+ /* name= */ "non_guest",
+ /* flags= */ 0,
+ )
+ private val GUEST_USER_INFO =
+ UserInfo(
+ /* id= */ 669,
+ /* name= */ "guest",
+ /* iconPath= */ "",
+ /* flags= */ 0,
+ UserManager.USER_TYPE_FULL_GUEST,
+ )
+ private val EPHEMERAL_GUEST_USER_INFO =
+ UserInfo(
+ /* id= */ 669,
+ /* name= */ "guest",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_EPHEMERAL,
+ UserManager.USER_TYPE_FULL_GUEST,
+ )
+ private val ALL_USERS =
+ listOf(
+ NON_GUEST_USER_INFO,
+ GUEST_USER_INFO,
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
new file mode 100644
index 000000000000..593ce1f0a2f5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.systemui.user.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class RefreshUsersSchedulerTest : SysuiTestCase() {
+
+ private lateinit var underTest: RefreshUsersScheduler
+
+ private lateinit var repository: FakeUserRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ repository = FakeUserRepository()
+ }
+
+ @Test
+ fun `pause - prevents the next refresh from happening`() =
+ runBlocking(IMMEDIATE) {
+ underTest =
+ RefreshUsersScheduler(
+ applicationScope = this,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ )
+ underTest.pause()
+
+ underTest.refreshIfNotPaused()
+ assertThat(repository.refreshUsersCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun `unpauseAndRefresh - forces the refresh even when paused`() =
+ runBlocking(IMMEDIATE) {
+ underTest =
+ RefreshUsersScheduler(
+ applicationScope = this,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ )
+ underTest.pause()
+
+ underTest.unpauseAndRefresh()
+
+ assertThat(repository.refreshUsersCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun `refreshIfNotPaused - refreshes when not paused`() =
+ runBlocking(IMMEDIATE) {
+ underTest =
+ RefreshUsersScheduler(
+ applicationScope = this,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ )
+ underTest.refreshIfNotPaused()
+
+ assertThat(repository.refreshUsersCallCount).isEqualTo(1)
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
new file mode 100644
index 000000000000..3d5695a09ebc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
@@ -0,0 +1,658 @@
+/*
+ * 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.systemui.user.domain.interactor
+
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.internal.R.drawable.ic_account_circle
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserInteractorRefactoredTest : UserInteractorTest() {
+
+ override fun isRefactored(): Boolean {
+ return true
+ }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+
+ overrideResource(R.drawable.ic_account_circle, GUEST_ICON)
+ overrideResource(R.dimen.max_avatar_size, 10)
+ overrideResource(
+ com.android.internal.R.string.config_supervisedUserCreationPackage,
+ SUPERVISED_USER_CREATION_APP_PACKAGE,
+ )
+ whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
+ whenever(manager.canAddMoreUsers(any())).thenReturn(true)
+ }
+
+ @Test
+ fun `users - switcher enabled`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 3, includeGuest = true)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+ var value: List<UserModel>? = null
+ val job = underTest.users.onEach { value = it }.launchIn(this)
+ assertUsers(models = value, count = 3, includeGuest = true)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `users - switches to second user`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+ var value: List<UserModel>? = null
+ val job = underTest.users.onEach { value = it }.launchIn(this)
+ userRepository.setSelectedUserInfo(userInfos[1])
+
+ assertUsers(models = value, count = 2, selectedIndex = 1)
+ job.cancel()
+ }
+
+ @Test
+ fun `users - switcher not enabled`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
+
+ var value: List<UserModel>? = null
+ val job = underTest.users.onEach { value = it }.launchIn(this)
+ assertUsers(models = value, count = 1)
+
+ job.cancel()
+ }
+
+ @Test
+ fun selectedUser() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+ var value: UserModel? = null
+ val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
+ assertUser(value, id = 0, isSelected = true)
+
+ userRepository.setSelectedUserInfo(userInfos[1])
+ assertUser(value, id = 1, isSelected = true)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device unlocked`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device unlocked user not primary - empty list`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[1])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(emptyList<UserActionModel>())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device unlocked user is guest - empty list`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = true)
+ assertThat(userInfos[1].isGuest).isTrue()
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[1])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(emptyList<UserActionModel>())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device locked add from lockscreen set - full list`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(
+ UserSwitcherSettingsModel(
+ isUserSwitcherEnabled = true,
+ isAddUsersFromLockscreen = true,
+ )
+ )
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device locked - only guest action is shown`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(true)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(listOf(UserActionModel.ENTER_GUEST_MODE))
+
+ job.cancel()
+ }
+
+ @Test
+ fun `executeAction - add user - dialog shown`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ keyguardRepository.setKeyguardShowing(false)
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.executeAction(UserActionModel.ADD_USER)
+ assertThat(dialogRequest)
+ .isEqualTo(
+ ShowDialogRequestModel.ShowAddUserDialog(
+ userHandle = userInfos[0].userHandle,
+ isKeyguardShowing = false,
+ showEphemeralMessage = false,
+ )
+ )
+
+ underTest.onDialogShown()
+ assertThat(dialogRequest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `executeAction - add supervised user - starts activity`() =
+ runBlocking(IMMEDIATE) {
+ underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
+
+ val intentCaptor = kotlinArgumentCaptor<Intent>()
+ verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+ assertThat(intentCaptor.value.action)
+ .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER)
+ assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE)
+ }
+
+ @Test
+ fun `executeAction - navigate to manage users`() =
+ runBlocking(IMMEDIATE) {
+ underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+
+ val intentCaptor = kotlinArgumentCaptor<Intent>()
+ verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+ assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
+ }
+
+ @Test
+ fun `executeAction - guest mode`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
+ whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
+ val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
+ val showDialogsJob =
+ underTest.dialogShowRequests
+ .onEach {
+ dialogRequests.add(it)
+ if (it != null) {
+ underTest.onDialogShown()
+ }
+ }
+ .launchIn(this)
+ val dismissDialogsJob =
+ underTest.dialogDismissRequests
+ .onEach {
+ if (it != null) {
+ underTest.onDialogDismissed()
+ }
+ }
+ .launchIn(this)
+
+ underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+
+ assertThat(dialogRequests)
+ .contains(
+ ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
+ )
+ verify(activityManager).switchUser(guestUserInfo.id)
+
+ showDialogsJob.cancel()
+ dismissDialogsJob.cancel()
+ }
+
+ @Test
+ fun `selectUser - already selected guest re-selected - exit guest dialog`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = true)
+ val guestUserInfo = userInfos[1]
+ assertThat(guestUserInfo.isGuest).isTrue()
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(guestUserInfo)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.selectUser(newlySelectedUserId = guestUserInfo.id)
+
+ assertThat(dialogRequest)
+ .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = true)
+ val guestUserInfo = userInfos[1]
+ assertThat(guestUserInfo.isGuest).isTrue()
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(guestUserInfo)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.selectUser(newlySelectedUserId = userInfos[0].id)
+
+ assertThat(dialogRequest)
+ .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun `selectUser - not currently guest - switches users`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.selectUser(newlySelectedUserId = userInfos[1].id)
+
+ assertThat(dialogRequest).isNull()
+ verify(activityManager).switchUser(userInfos[1].id)
+ job.cancel()
+ }
+
+ @Test
+ fun `Telephony call state changes - refreshes users`() =
+ runBlocking(IMMEDIATE) {
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ telephonyRepository.setCallState(1)
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `User switched broadcast`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ val callback1: UserInteractor.UserCallback = mock()
+ val callback2: UserInteractor.UserCallback = mock()
+ underTest.addCallback(callback1)
+ underTest.addCallback(callback2)
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ userRepository.setSelectedUserInfo(userInfos[1])
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_SWITCHED)
+ .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
+ )
+ }
+
+ verify(callback1).onUserStateChanged()
+ verify(callback2).onUserStateChanged()
+ assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `User info changed broadcast`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_INFO_CHANGED),
+ )
+ }
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `System user unlocked broadcast - refresh users`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED)
+ .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
+ )
+ }
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `Non-system user unlocked broadcast - do not refresh users`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337),
+ )
+ }
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount)
+ }
+
+ @Test
+ fun userRecords() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 3, includeGuest = false)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ keyguardRepository.setKeyguardShowing(false)
+
+ testCoroutineScope.advanceUntilIdle()
+
+ assertRecords(
+ records = underTest.userRecords.value,
+ userIds = listOf(0, 1, 2),
+ selectedUserIndex = 0,
+ includeGuest = false,
+ expectedActions =
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ ),
+ )
+ }
+
+ @Test
+ fun selectedUserRecord() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 3, includeGuest = true)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ keyguardRepository.setKeyguardShowing(false)
+
+ assertRecordForUser(
+ record = underTest.selectedUserRecord.value,
+ id = 0,
+ hasPicture = true,
+ isCurrent = true,
+ isSwitchToEnabled = true,
+ )
+ }
+
+ private fun assertUsers(
+ models: List<UserModel>?,
+ count: Int,
+ selectedIndex: Int = 0,
+ includeGuest: Boolean = false,
+ ) {
+ checkNotNull(models)
+ assertThat(models.size).isEqualTo(count)
+ models.forEachIndexed { index, model ->
+ assertUser(
+ model = model,
+ id = index,
+ isSelected = index == selectedIndex,
+ isGuest = includeGuest && index == count - 1
+ )
+ }
+ }
+
+ private fun assertUser(
+ model: UserModel?,
+ id: Int,
+ isSelected: Boolean = false,
+ isGuest: Boolean = false,
+ ) {
+ checkNotNull(model)
+ assertThat(model.id).isEqualTo(id)
+ assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id"))
+ assertThat(model.isSelected).isEqualTo(isSelected)
+ assertThat(model.isSelectable).isTrue()
+ assertThat(model.isGuest).isEqualTo(isGuest)
+ }
+
+ private fun assertRecords(
+ records: List<UserRecord>,
+ userIds: List<Int>,
+ selectedUserIndex: Int = 0,
+ includeGuest: Boolean = false,
+ expectedActions: List<UserActionModel> = emptyList(),
+ ) {
+ assertThat(records.size >= userIds.size).isTrue()
+ userIds.indices.forEach { userIndex ->
+ val record = records[userIndex]
+ assertThat(record.info).isNotNull()
+ val isGuest = includeGuest && userIndex == userIds.size - 1
+ assertRecordForUser(
+ record = record,
+ id = userIds[userIndex],
+ hasPicture = !isGuest,
+ isCurrent = userIndex == selectedUserIndex,
+ isGuest = isGuest,
+ isSwitchToEnabled = true,
+ )
+ }
+
+ assertThat(records.size - userIds.size).isEqualTo(expectedActions.size)
+ (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex ->
+ val record = records[actionIndex]
+ assertThat(record.info).isNull()
+ assertRecordForAction(
+ record = record,
+ type = expectedActions[actionIndex - userIds.size],
+ )
+ }
+ }
+
+ private fun assertRecordForUser(
+ record: UserRecord?,
+ id: Int? = null,
+ hasPicture: Boolean = false,
+ isCurrent: Boolean = false,
+ isGuest: Boolean = false,
+ isSwitchToEnabled: Boolean = false,
+ ) {
+ checkNotNull(record)
+ assertThat(record.info?.id).isEqualTo(id)
+ assertThat(record.picture != null).isEqualTo(hasPicture)
+ assertThat(record.isCurrent).isEqualTo(isCurrent)
+ assertThat(record.isGuest).isEqualTo(isGuest)
+ assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
+ }
+
+ private fun assertRecordForAction(
+ record: UserRecord,
+ type: UserActionModel,
+ ) {
+ assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
+ assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
+ assertThat(record.isAddSupervisedUser)
+ .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
+ }
+
+ private fun createUserInfos(
+ count: Int,
+ includeGuest: Boolean,
+ ): List<UserInfo> {
+ return (0 until count).map { index ->
+ val isGuest = includeGuest && index == count - 1
+ createUserInfo(
+ id = index,
+ name =
+ if (isGuest) {
+ "guest"
+ } else {
+ "user_$index"
+ },
+ isPrimary = !isGuest && index == 0,
+ isGuest = isGuest,
+ )
+ }
+ }
+
+ private fun createUserInfo(
+ id: Int,
+ name: String,
+ isPrimary: Boolean = false,
+ isGuest: Boolean = false,
+ ): UserInfo {
+ return UserInfo(
+ id,
+ name,
+ /* iconPath= */ "",
+ /* flags= */ if (isPrimary) {
+ UserInfo.FLAG_PRIMARY
+ } else {
+ 0
+ },
+ if (isGuest) {
+ UserManager.USER_TYPE_FULL_GUEST
+ } else {
+ UserManager.USER_TYPE_FULL_SYSTEM
+ },
+ )
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ private val GUEST_ICON: Drawable = mock()
+ private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index e914e2e0a1da..8465f4f46d62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -17,51 +17,61 @@
package com.android.systemui.user.domain.interactor
-import androidx.test.filters.SmallTest
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.os.UserManager
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.nullable
-import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlinx.coroutines.test.TestCoroutineScope
import org.mockito.Mock
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-@SmallTest
-@RunWith(JUnit4::class)
-class UserInteractorTest : SysuiTestCase() {
+abstract class UserInteractorTest : SysuiTestCase() {
- @Mock private lateinit var controller: UserSwitcherController
- @Mock private lateinit var activityStarter: ActivityStarter
+ @Mock protected lateinit var controller: UserSwitcherController
+ @Mock protected lateinit var activityStarter: ActivityStarter
+ @Mock protected lateinit var manager: UserManager
+ @Mock protected lateinit var activityManager: ActivityManager
+ @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock protected lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock protected lateinit var uiEventLogger: UiEventLogger
- private lateinit var underTest: UserInteractor
+ protected lateinit var underTest: UserInteractor
- private lateinit var userRepository: FakeUserRepository
- private lateinit var keyguardRepository: FakeKeyguardRepository
+ protected lateinit var testCoroutineScope: TestCoroutineScope
+ protected lateinit var userRepository: FakeUserRepository
+ protected lateinit var keyguardRepository: FakeKeyguardRepository
+ protected lateinit var telephonyRepository: FakeTelephonyRepository
- @Before
- fun setUp() {
+ abstract fun isRefactored(): Boolean
+
+ open fun setUp() {
MockitoAnnotations.initMocks(this)
userRepository = FakeUserRepository()
keyguardRepository = FakeKeyguardRepository()
+ telephonyRepository = FakeTelephonyRepository()
+ testCoroutineScope = TestCoroutineScope()
+ val refreshUsersScheduler =
+ RefreshUsersScheduler(
+ applicationScope = testCoroutineScope,
+ mainDispatcher = IMMEDIATE,
+ repository = userRepository,
+ )
underTest =
UserInteractor(
+ applicationContext = context,
repository = userRepository,
controller = controller,
activityStarter = activityStarter,
@@ -69,142 +79,34 @@ class UserInteractorTest : SysuiTestCase() {
KeyguardInteractor(
repository = keyguardRepository,
),
- )
- }
-
- @Test
- fun `actions - not actionable when locked and locked - no actions`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(UserActionModel.values().toList())
- userRepository.setActionableWhenLocked(false)
- keyguardRepository.setKeyguardShowing(true)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions).isEmpty()
- job.cancel()
- }
-
- @Test
- fun `actions - not actionable when locked and not locked`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
- )
- userRepository.setActionableWhenLocked(false)
- keyguardRepository.setKeyguardShowing(false)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions)
- .isEqualTo(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
- )
- )
- job.cancel()
- }
-
- @Test
- fun `actions - actionable when locked and not locked`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
- )
- userRepository.setActionableWhenLocked(true)
- keyguardRepository.setKeyguardShowing(false)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions)
- .isEqualTo(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ featureFlags =
+ FakeFeatureFlags().apply {
+ set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored())
+ },
+ manager = manager,
+ applicationScope = testCoroutineScope,
+ telephonyInteractor =
+ TelephonyInteractor(
+ repository = telephonyRepository,
+ ),
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ backgroundDispatcher = IMMEDIATE,
+ activityManager = activityManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ guestUserInteractor =
+ GuestUserInteractor(
+ applicationContext = context,
+ applicationScope = testCoroutineScope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ manager = manager,
+ repository = userRepository,
+ deviceProvisionedController = deviceProvisionedController,
+ devicePolicyManager = devicePolicyManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ uiEventLogger = uiEventLogger,
)
- )
- job.cancel()
- }
-
- @Test
- fun `actions - actionable when locked and locked`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
)
- userRepository.setActionableWhenLocked(true)
- keyguardRepository.setKeyguardShowing(true)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions)
- .isEqualTo(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
- )
- )
- job.cancel()
- }
-
- @Test
- fun selectUser() {
- val userId = 3
-
- underTest.selectUser(userId)
-
- verify(controller).onUserSelected(eq(userId), nullable())
- }
-
- @Test
- fun `executeAction - guest`() {
- underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
-
- verify(controller).createAndSwitchToGuestUser(nullable())
- }
-
- @Test
- fun `executeAction - add user`() {
- underTest.executeAction(UserActionModel.ADD_USER)
-
- verify(controller).showAddUserDialog(nullable())
- }
-
- @Test
- fun `executeAction - add supervised user`() {
- underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
-
- verify(controller).startSupervisedUserActivity()
- }
-
- @Test
- fun `executeAction - manage users`() {
- underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-
- verify(activityStarter).startActivity(any(), anyBoolean())
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
new file mode 100644
index 000000000000..c3a9705bf6ba
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.systemui.user.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.nullable
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+open class UserInteractorUnrefactoredTest : UserInteractorTest() {
+
+ override fun isRefactored(): Boolean {
+ return false
+ }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ }
+
+ @Test
+ fun `actions - not actionable when locked and locked - no actions`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(UserActionModel.values().toList())
+ userRepository.setActionableWhenLocked(false)
+ keyguardRepository.setKeyguardShowing(true)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions).isEmpty()
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - not actionable when locked and not locked`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+ userRepository.setActionableWhenLocked(false)
+ keyguardRepository.setKeyguardShowing(false)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ )
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - actionable when locked and not locked`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+ userRepository.setActionableWhenLocked(true)
+ keyguardRepository.setKeyguardShowing(false)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ )
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - actionable when locked and locked`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+ userRepository.setActionableWhenLocked(true)
+ keyguardRepository.setKeyguardShowing(true)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ )
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun selectUser() {
+ val userId = 3
+
+ underTest.selectUser(userId)
+
+ verify(controller).onUserSelected(eq(userId), nullable())
+ }
+
+ @Test
+ fun `executeAction - guest`() {
+ underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+
+ verify(controller).createAndSwitchToGuestUser(nullable())
+ }
+
+ @Test
+ fun `executeAction - add user`() {
+ underTest.executeAction(UserActionModel.ADD_USER)
+
+ verify(controller).showAddUserDialog(nullable())
+ }
+
+ @Test
+ fun `executeAction - add supervised user`() {
+ underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
+
+ verify(controller).startSupervisedUserActivity()
+ }
+
+ @Test
+ fun `executeAction - manage users`() {
+ underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+
+ verify(activityStarter).startActivity(any(), anyBoolean())
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index ef4500df3600..0344e3f991e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -17,17 +17,28 @@
package com.android.systemui.user.ui.viewmodel
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
import android.graphics.drawable.Drawable
+import android.os.UserManager
import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Text
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.power.data.repository.FakePowerRepository
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
@@ -38,6 +49,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.yield
import org.junit.Before
import org.junit.Test
@@ -52,6 +64,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
@Mock private lateinit var controller: UserSwitcherController
@Mock private lateinit var activityStarter: ActivityStarter
+ @Mock private lateinit var activityManager: ActivityManager
+ @Mock private lateinit var manager: UserManager
+ @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var uiEventLogger: UiEventLogger
private lateinit var underTest: UserSwitcherViewModel
@@ -66,22 +83,60 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
userRepository = FakeUserRepository()
keyguardRepository = FakeKeyguardRepository()
powerRepository = FakePowerRepository()
+ val featureFlags = FakeFeatureFlags()
+ featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true)
+ val scope = TestCoroutineScope()
+ val refreshUsersScheduler =
+ RefreshUsersScheduler(
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ repository = userRepository,
+ )
+ val guestUserInteractor =
+ GuestUserInteractor(
+ applicationContext = context,
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ manager = manager,
+ repository = userRepository,
+ deviceProvisionedController = deviceProvisionedController,
+ devicePolicyManager = devicePolicyManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ uiEventLogger = uiEventLogger,
+ )
+
underTest =
UserSwitcherViewModel.Factory(
userInteractor =
UserInteractor(
+ applicationContext = context,
repository = userRepository,
controller = controller,
activityStarter = activityStarter,
keyguardInteractor =
KeyguardInteractor(
repository = keyguardRepository,
- )
+ ),
+ featureFlags = featureFlags,
+ manager = manager,
+ applicationScope = scope,
+ telephonyInteractor =
+ TelephonyInteractor(
+ repository = FakeTelephonyRepository(),
+ ),
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ backgroundDispatcher = IMMEDIATE,
+ activityManager = activityManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ guestUserInteractor = guestUserInteractor,
),
powerInteractor =
PowerInteractor(
repository = powerRepository,
),
+ featureFlags = featureFlags,
+ guestUserInteractor = guestUserInteractor,
)
.create(UserSwitcherViewModel::class.java)
}
@@ -97,6 +152,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE,
isSelected = true,
isSelectable = true,
+ isGuest = false,
),
UserModel(
id = 1,
@@ -104,6 +160,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE,
isSelected = false,
isSelectable = true,
+ isGuest = false,
),
UserModel(
id = 2,
@@ -111,6 +168,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE,
isSelected = false,
isSelectable = false,
+ isGuest = false,
),
)
)
@@ -260,7 +318,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
job.cancel()
}
- private fun setUsers(count: Int) {
+ private suspend fun setUsers(count: Int) {
userRepository.setUsers(
(0 until count).map { index ->
UserModel(
@@ -269,6 +327,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
image = USER_IMAGE,
isSelected = index == 0,
isSelectable = true,
+ isGuest = false,
)
}
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
index 53dcc8d269c9..bb646f09b774 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
@@ -37,10 +37,18 @@ class FakeBroadcastDispatcher(
dumpManager: DumpManager,
logger: BroadcastDispatcherLogger,
userTracker: UserTracker
-) : BroadcastDispatcher(
- context, looper, executor, dumpManager, logger, userTracker, PendingRemovalStore(logger)) {
+) :
+ BroadcastDispatcher(
+ context,
+ looper,
+ executor,
+ dumpManager,
+ logger,
+ userTracker,
+ PendingRemovalStore(logger)
+ ) {
- private val registeredReceivers = ArraySet<BroadcastReceiver>()
+ val registeredReceivers = ArraySet<BroadcastReceiver>()
override fun registerReceiverWithHandler(
receiver: BroadcastReceiver,
@@ -78,4 +86,4 @@ class FakeBroadcastDispatcher(
}
registeredReceivers.clear()
}
-} \ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 42b434a9deaf..725b1f41372c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -44,6 +44,10 @@ class FakeKeyguardRepository : KeyguardRepository {
private val _dozeAmount = MutableStateFlow(0f)
override val dozeAmount: Flow<Float> = _dozeAmount
+ override fun isKeyguardShowing(): Boolean {
+ return _isKeyguardShowing.value
+ }
+
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.tryEmit(animate)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index b2b176420e40..9726bf83b263 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -26,20 +26,24 @@ import java.util.concurrent.Executor
/** A fake [UserTracker] to be used in tests. */
class FakeUserTracker(
- userId: Int = 0,
- userHandle: UserHandle = UserHandle.of(userId),
- userInfo: UserInfo = mock(),
- userProfiles: List<UserInfo> = emptyList(),
+ private var _userId: Int = 0,
+ private var _userHandle: UserHandle = UserHandle.of(_userId),
+ private var _userInfo: UserInfo = mock(),
+ private var _userProfiles: List<UserInfo> = emptyList(),
userContentResolver: ContentResolver = MockContentResolver(),
userContext: Context = mock(),
private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
) : UserTracker {
val callbacks = mutableListOf<UserTracker.Callback>()
- override val userId: Int = userId
- override val userHandle: UserHandle = userHandle
- override val userInfo: UserInfo = userInfo
- override val userProfiles: List<UserInfo> = userProfiles
+ override val userId: Int
+ get() = _userId
+ override val userHandle: UserHandle
+ get() = _userHandle
+ override val userInfo: UserInfo
+ get() = _userInfo
+ override val userProfiles: List<UserInfo>
+ get() = _userProfiles
override val userContentResolver: ContentResolver = userContentResolver
override val userContext: Context = userContext
@@ -55,4 +59,13 @@ class FakeUserTracker(
override fun createCurrentUserContext(context: Context): Context {
return onCreateCurrentUserContext(context)
}
+
+ fun set(userInfos: List<UserInfo>, selectedUserIndex: Int) {
+ _userProfiles = userInfos
+ _userInfo = userInfos[selectedUserIndex]
+ _userId = _userInfo.id
+ _userHandle = UserHandle.of(_userId)
+
+ callbacks.forEach { it.onUserChanged(_userId, userContext) }
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
new file mode 100644
index 000000000000..59f24ef2a706
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.systemui.telephony.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeTelephonyRepository : TelephonyRepository {
+
+ private val _callState = MutableStateFlow(0)
+ override val callState: Flow<Int> = _callState.asStateFlow()
+
+ fun setCallState(value: Int) {
+ _callState.value = value
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 20f1e367944f..4df8aa42ea2f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -17,12 +17,18 @@
package com.android.systemui.user.data.repository
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
+import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.yield
class FakeUserRepository : UserRepository {
@@ -34,21 +40,71 @@ class FakeUserRepository : UserRepository {
private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList())
override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow()
+ private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
+ override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
+ _userSwitcherSettings.asStateFlow()
+
+ private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList())
+ override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow()
+
+ private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
+ override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+
+ override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+
private val _isActionableWhenLocked = MutableStateFlow(false)
override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow()
private var _isGuestUserAutoCreated: Boolean = false
override val isGuestUserAutoCreated: Boolean
get() = _isGuestUserAutoCreated
- private var _isGuestUserResetting: Boolean = false
- override val isGuestUserResetting: Boolean
- get() = _isGuestUserResetting
+
+ override var isGuestUserResetting: Boolean = false
+
+ override val isGuestUserCreationScheduled = AtomicBoolean()
+
+ override var secondaryUserId: Int = UserHandle.USER_NULL
+
+ override var isRefreshUsersPaused: Boolean = false
+
+ var refreshUsersCallCount: Int = 0
+ private set
+
+ override fun refreshUsers() {
+ refreshUsersCallCount++
+ }
+
+ override fun getSelectedUserInfo(): UserInfo {
+ return checkNotNull(_selectedUserInfo.value)
+ }
+
+ override fun isSimpleUserSwitcher(): Boolean {
+ return _userSwitcherSettings.value.isSimpleUserSwitcher
+ }
+
+ fun setUserInfos(infos: List<UserInfo>) {
+ _userInfos.value = infos
+ }
+
+ suspend fun setSelectedUserInfo(userInfo: UserInfo) {
+ check(_userInfos.value.contains(userInfo)) {
+ "Cannot select the following user, it is not in the list of user infos: $userInfo!"
+ }
+
+ _selectedUserInfo.value = userInfo
+ yield()
+ }
+
+ suspend fun setSettings(settings: UserSwitcherSettingsModel) {
+ _userSwitcherSettings.value = settings
+ yield()
+ }
fun setUsers(models: List<UserModel>) {
_users.value = models
}
- fun setSelectedUser(userId: Int) {
+ suspend fun setSelectedUser(userId: Int) {
check(_users.value.find { it.id == userId } != null) {
"Cannot select a user with ID $userId - no user with that ID found!"
}
@@ -62,6 +118,7 @@ class FakeUserRepository : UserRepository {
}
}
)
+ yield()
}
fun setActions(models: List<UserActionModel>) {
@@ -75,8 +132,4 @@ class FakeUserRepository : UserRepository {
fun setGuestUserAutoCreated(value: Boolean) {
_isGuestUserAutoCreated = value
}
-
- fun setGuestUserResetting(value: Boolean) {
- _isGuestUserResetting = value
- }
}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 56a5a8f829b7..0426c79358ba 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -1175,6 +1175,9 @@ public class CompanionDeviceManagerService extends SystemService {
}
private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
+ if (packageInfo == null) {
+ return;
+ }
if (containsEither(packageInfo.requestedPermissions,
android.Manifest.permission.RUN_IN_BACKGROUND,
android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
diff --git a/services/companion/java/com/android/server/companion/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java
index 9bad45bd82f6..3ab4aa89406b 100644
--- a/services/companion/java/com/android/server/companion/PackageUtils.java
+++ b/services/companion/java/com/android/server/companion/PackageUtils.java
@@ -54,12 +54,19 @@ final class PackageUtils {
private static final String PROPERTY_PRIMARY_TAG =
"android.companion.PROPERTY_PRIMARY_COMPANION_DEVICE_SERVICE";
- static @Nullable PackageInfo getPackageInfo(@NonNull Context context,
+ @Nullable
+ static PackageInfo getPackageInfo(@NonNull Context context,
@UserIdInt int userId, @NonNull String packageName) {
final PackageManager pm = context.getPackageManager();
final PackageInfoFlags flags = PackageInfoFlags.of(GET_PERMISSIONS | GET_CONFIGURATIONS);
- return Binder.withCleanCallingIdentity(() ->
- pm.getPackageInfoAsUser(packageName, flags , userId));
+ return Binder.withCleanCallingIdentity(() -> {
+ try {
+ return pm.getPackageInfoAsUser(packageName, flags, userId);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.e(TAG, "Package [" + packageName + "] is not found.");
+ return null;
+ }
+ });
}
static void enforceUsesCompanionDeviceFeature(@NonNull Context context,
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 4204162f3d98..5f27f598ee4e 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -95,6 +95,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
private final AssociationInfo mAssociationInfo;
private final PendingTrampolineCallback mPendingTrampolineCallback;
private final int mOwnerUid;
+ private final int mDeviceId;
private final InputController mInputController;
private VirtualAudioController mVirtualAudioController;
@VisibleForTesting
@@ -140,19 +141,40 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
private final SparseArray<GenericWindowPolicyController> mWindowPolicyControllers =
new SparseArray<>();
- VirtualDeviceImpl(Context context, AssociationInfo associationInfo,
- IBinder token, int ownerUid, OnDeviceCloseListener listener,
+ VirtualDeviceImpl(
+ Context context,
+ AssociationInfo associationInfo,
+ IBinder token,
+ int ownerUid,
+ int deviceId,
+ OnDeviceCloseListener listener,
PendingTrampolineCallback pendingTrampolineCallback,
IVirtualDeviceActivityListener activityListener,
Consumer<ArraySet<Integer>> runningAppsChangedCallback,
VirtualDeviceParams params) {
- this(context, associationInfo, token, ownerUid, /* inputController= */ null, listener,
- pendingTrampolineCallback, activityListener, runningAppsChangedCallback, params);
+ this(
+ context,
+ associationInfo,
+ token,
+ ownerUid,
+ deviceId,
+ /* inputController= */ null,
+ listener,
+ pendingTrampolineCallback,
+ activityListener,
+ runningAppsChangedCallback,
+ params);
}
@VisibleForTesting
- VirtualDeviceImpl(Context context, AssociationInfo associationInfo, IBinder token,
- int ownerUid, InputController inputController, OnDeviceCloseListener listener,
+ VirtualDeviceImpl(
+ Context context,
+ AssociationInfo associationInfo,
+ IBinder token,
+ int ownerUid,
+ int deviceId,
+ InputController inputController,
+ OnDeviceCloseListener listener,
PendingTrampolineCallback pendingTrampolineCallback,
IVirtualDeviceActivityListener activityListener,
Consumer<ArraySet<Integer>> runningAppsChangedCallback,
@@ -164,6 +186,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
mActivityListener = activityListener;
mRunningAppsChangedCallback = runningAppsChangedCallback;
mOwnerUid = ownerUid;
+ mDeviceId = deviceId;
mAppToken = token;
mParams = params;
if (inputController == null) {
@@ -199,6 +222,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
return mAssociationInfo.getDisplayName();
}
+ /** Returns the unique device ID of this device. */
+ @Override // Binder call
+ public int getDeviceId() {
+ return mDeviceId;
+ }
+
@Override // Binder call
public int getAssociationId() {
return mAssociationInfo.getId();
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 2b644fe8f7ba..06dfeabfe832 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -60,12 +60,12 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
@SuppressLint("LongLogTag")
public class VirtualDeviceManagerService extends SystemService {
- private static final boolean DEBUG = false;
private static final String TAG = "VirtualDeviceManagerService";
private final Object mVirtualDeviceManagerLock = new Object();
@@ -73,6 +73,10 @@ public class VirtualDeviceManagerService extends SystemService {
private final VirtualDeviceManagerInternal mLocalService;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final PendingTrampolineMap mPendingTrampolines = new PendingTrampolineMap(mHandler);
+
+ private static AtomicInteger sNextUniqueIndex = new AtomicInteger(
+ VirtualDeviceManager.DEFAULT_DEVICE_ID + 1);
+
/**
* Mapping from user IDs to CameraAccessControllers.
*/
@@ -260,8 +264,10 @@ public class VirtualDeviceManagerService extends SystemService {
final int userId = UserHandle.getUserId(callingUid);
final CameraAccessController cameraAccessController =
mCameraAccessControllers.get(userId);
+ final int uniqueId = sNextUniqueIndex.getAndIncrement();
+
VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(),
- associationInfo, token, callingUid,
+ associationInfo, token, callingUid, uniqueId,
new VirtualDeviceImpl.OnDeviceCloseListener() {
@Override
public void onClose(int associationId) {
diff --git a/services/core/java/com/android/server/AlarmManagerInternal.java b/services/core/java/com/android/server/AlarmManagerInternal.java
index b2abdbd144b8..67aa2b9a89bb 100644
--- a/services/core/java/com/android/server/AlarmManagerInternal.java
+++ b/services/core/java/com/android/server/AlarmManagerInternal.java
@@ -16,8 +16,10 @@
package com.android.server;
+import android.annotation.CurrentTimeMillisLong;
import android.app.PendingIntent;
+import com.android.server.SystemClockTime.TimeConfidence;
import com.android.server.SystemTimeZone.TimeZoneConfidence;
public interface AlarmManagerInternal {
@@ -59,4 +61,15 @@ public interface AlarmManagerInternal {
* for details
*/
void setTimeZone(String tzId, @TimeZoneConfidence int confidence);
+
+ /**
+ * Sets the device's current time and time confidence.
+ *
+ * @param unixEpochTimeMillis the time
+ * @param confidence the confidence that {@code unixEpochTimeMillis} is correct, see {@link
+ * TimeConfidence} for details
+ * @param logMsg the reason the time is being changed, for bug report logging
+ */
+ void setTime(@CurrentTimeMillisLong long unixEpochTimeMillis, @TimeConfidence int confidence,
+ String logMsg);
}
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 5b1f7409f439..83d527e1d68b 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -147,7 +147,6 @@ import com.android.internal.util.DumpUtils;
import com.android.internal.util.HexDump;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
-import com.android.internal.widget.LockPatternUtils;
import com.android.server.pm.Installer;
import com.android.server.pm.UserManagerInternal;
import com.android.server.storage.AppFuseBridge;
@@ -557,7 +556,6 @@ class StorageManagerService extends IStorageManager.Stub
private IAppOpsService mIAppOpsService;
private final Callbacks mCallbacks;
- private final LockPatternUtils mLockPatternUtils;
private static final String ANR_DELAY_MILLIS_DEVICE_CONFIG_KEY =
"anr_delay_millis";
@@ -1808,7 +1806,6 @@ class StorageManagerService extends IStorageManager.Stub
ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, false);
mContext = context;
mCallbacks = new Callbacks(FgThread.get().getLooper());
- mLockPatternUtils = new LockPatternUtils(mContext);
HandlerThread hthread = new HandlerThread(TAG);
hthread.start();
@@ -3069,96 +3066,21 @@ class StorageManagerService extends IStorageManager.Stub
}
}
- private String encodeBytes(byte[] bytes) {
- if (ArrayUtils.isEmpty(bytes)) {
- return "!";
- } else {
- return HexDump.toHexString(bytes);
- }
- }
-
+ /* Only for use by LockSettingsService */
@android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
- /*
- * Add this secret to the set of ways we can recover a user's disk
- * encryption key. Changing the secret for a disk encryption key is done in
- * two phases. First, this method is called to add the new secret binding.
- * Second, fixateNewestUserKeyAuth is called to delete all other bindings.
- * This allows other places where a credential is used, such as Gatekeeper,
- * to be updated between the two calls.
- */
@Override
- public void addUserKeyAuth(int userId, int serialNumber, byte[] secret) {
-
- try {
- mVold.addUserKeyAuth(userId, serialNumber, encodeBytes(secret));
- } catch (Exception e) {
- Slog.wtf(TAG, e);
- }
+ public void setUserKeyProtection(@UserIdInt int userId, byte[] secret) throws RemoteException {
+ mVold.setUserKeyProtection(userId, HexDump.toHexString(secret));
}
+ /* Only for use by LockSettingsService */
@android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
- /*
- * Store a user's disk encryption key without secret binding. Removing the
- * secret for a disk encryption key is done in two phases. First, this
- * method is called to retrieve the key using the provided secret and store
- * it encrypted with a keystore key not bound to the user. Second,
- * fixateNewestUserKeyAuth is called to delete the key's other bindings.
- */
@Override
- public void clearUserKeyAuth(int userId, int serialNumber, byte[] secret) {
-
- try {
- mVold.clearUserKeyAuth(userId, serialNumber, encodeBytes(secret));
- } catch (Exception e) {
- Slog.wtf(TAG, e);
+ public void unlockUserKey(@UserIdInt int userId, int serialNumber, byte[] secret)
+ throws RemoteException {
+ if (StorageManager.isFileEncrypted()) {
+ mVold.unlockUserKey(userId, serialNumber, HexDump.toHexString(secret));
}
- }
-
- @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
- /*
- * Delete all bindings of a user's disk encryption key except the most
- * recently added one.
- */
- @Override
- public void fixateNewestUserKeyAuth(int userId) {
-
- try {
- mVold.fixateNewestUserKeyAuth(userId);
- } catch (Exception e) {
- Slog.wtf(TAG, e);
- }
- }
-
- @Override
- public void unlockUserKey(int userId, int serialNumber, byte[] secret) {
- boolean isFileEncrypted = StorageManager.isFileEncrypted();
- Slog.d(TAG, "unlockUserKey: " + userId
- + " isFileEncrypted: " + isFileEncrypted
- + " hasSecret: " + (secret != null));
- enforcePermission(android.Manifest.permission.STORAGE_INTERNAL);
-
- if (isUserKeyUnlocked(userId)) {
- Slog.d(TAG, "User " + userId + "'s CE storage is already unlocked");
- return;
- }
-
- if (isFileEncrypted) {
- // When a user has a secure lock screen, a secret is required to
- // unlock the key, so don't bother trying to unlock it without one.
- // This prevents misleading error messages from being logged.
- if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(secret)) {
- Slog.d(TAG, "Not unlocking user " + userId
- + "'s CE storage yet because a secret is needed");
- return;
- }
- try {
- mVold.unlockUserKey(userId, serialNumber, encodeBytes(secret));
- } catch (Exception e) {
- Slog.wtf(TAG, e);
- return;
- }
- }
-
synchronized (mLock) {
mLocalUnlockedUsers.append(userId);
}
diff --git a/services/core/java/com/android/server/SystemClockTime.java b/services/core/java/com/android/server/SystemClockTime.java
new file mode 100644
index 000000000000..46fbbb290ed2
--- /dev/null
+++ b/services/core/java/com/android/server/SystemClockTime.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 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;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Build;
+import android.os.Environment;
+import android.os.SystemProperties;
+import android.util.LocalLog;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * A set of static methods that encapsulate knowledge of how the system clock time and associated
+ * metadata are stored on Android.
+ */
+public final class SystemClockTime {
+
+ private static final String TAG = "SystemClockTime";
+
+ /**
+ * A log that records the decisions / decision metadata that affected the device's system clock
+ * time. This is logged in bug reports to assist with debugging issues with time.
+ */
+ @NonNull
+ private static final LocalLog sTimeDebugLog =
+ new LocalLog(30, false /* useLocalTimestamps */);
+
+
+ /**
+ * An annotation that indicates a "time confidence" value is expected.
+ *
+ * <p>The confidence indicates whether the time is expected to be correct. The confidence can be
+ * upgraded or downgraded over time. It can be used to decide whether a user could / should be
+ * asked to confirm the time. For example, during device set up low confidence would describe a
+ * time that has been initialized by default. The user may then be asked to confirm the time,
+ * moving it to a high confidence.
+ */
+ @Retention(SOURCE)
+ @Target(TYPE_USE)
+ @IntDef(prefix = "TIME_CONFIDENCE_",
+ value = { TIME_CONFIDENCE_LOW, TIME_CONFIDENCE_HIGH })
+ public @interface TimeConfidence {
+ }
+
+ /** Used when confidence is low and would (ideally) be confirmed by a user. */
+ public static final @TimeConfidence int TIME_CONFIDENCE_LOW = 0;
+
+ /**
+ * Used when confidence in the time is high and does not need to be confirmed by a user.
+ */
+ public static final @TimeConfidence int TIME_CONFIDENCE_HIGH = 100;
+
+ /**
+ * The confidence in the current time. Android's time confidence is held in memory because RTC
+ * hardware can forget / corrupt the time while the device is powered off. Therefore, on boot
+ * we can't assume the time is good, and so default it to "low" confidence until it is confirmed
+ * or explicitly set.
+ */
+ private static @TimeConfidence int sTimeConfidence = TIME_CONFIDENCE_LOW;
+
+ private static final long sNativeData = init();
+
+ private SystemClockTime() {
+ }
+
+ /**
+ * Sets the system clock time to a reasonable lower bound. Used during boot-up to ensure the
+ * device has a time that is better than a default like 1970-01-01.
+ */
+ public static void initializeIfRequired() {
+ // Use the most recent of Build.TIME, the root file system's timestamp, and the
+ // value of the ro.build.date.utc system property (which is in seconds).
+ final long systemBuildTime = Long.max(
+ 1000L * SystemProperties.getLong("ro.build.date.utc", -1L),
+ Long.max(Environment.getRootDirectory().lastModified(), Build.TIME));
+ long currentTimeMillis = getCurrentTimeMillis();
+ if (currentTimeMillis < systemBuildTime) {
+ String logMsg = "Current time only " + currentTimeMillis
+ + ", advancing to build time " + systemBuildTime;
+ Slog.i(TAG, logMsg);
+ setTimeAndConfidence(systemBuildTime, TIME_CONFIDENCE_LOW, logMsg);
+ }
+ }
+
+ /**
+ * Sets the system clock time and confidence. See also {@link #setConfidence(int, String)} for
+ * an alternative that only sets the confidence.
+ *
+ * @param unixEpochMillis the time to set
+ * @param confidence the confidence in {@code unixEpochMillis}. See {@link TimeConfidence} for
+ * details.
+ * @param logMsg a log message that can be included in bug reports that explains the update
+ */
+ public static void setTimeAndConfidence(
+ @CurrentTimeMillisLong long unixEpochMillis, int confidence, @NonNull String logMsg) {
+ synchronized (SystemClockTime.class) {
+ setTime(sNativeData, unixEpochMillis);
+ sTimeConfidence = confidence;
+ sTimeDebugLog.log(logMsg);
+ }
+ }
+
+ /**
+ * Sets the system clock confidence. See also {@link #setTimeAndConfidence(long, int, String)}
+ * for an alternative that sets the time and confidence.
+ *
+ * @param confidence the confidence in the system clock time. See {@link TimeConfidence} for
+ * details.
+ * @param logMsg a log message that can be included in bug reports that explains the update
+ */
+ public static void setConfidence(@TimeConfidence int confidence, @NonNull String logMsg) {
+ synchronized (SystemClockTime.class) {
+ sTimeConfidence = confidence;
+ sTimeDebugLog.log(logMsg);
+ }
+ }
+
+ /**
+ * Returns the system clock time. The same as {@link System#currentTimeMillis()}.
+ */
+ private static @CurrentTimeMillisLong long getCurrentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * Returns the system clock confidence. See {@link TimeConfidence} for details.
+ */
+ public static @TimeConfidence int getTimeConfidence() {
+ synchronized (SystemClockTime.class) {
+ return sTimeConfidence;
+ }
+ }
+
+ /**
+ * Adds an entry to the system time debug log that is included in bug reports. This method is
+ * intended to be used to record event that may lead to a time change, e.g. config or mode
+ * changes.
+ */
+ public static void addDebugLogEntry(@NonNull String logMsg) {
+ sTimeDebugLog.log(logMsg);
+ }
+
+ /**
+ * Dumps information about recent time / confidence changes to the supplied writer.
+ */
+ public static void dump(PrintWriter writer) {
+ sTimeDebugLog.dump(writer);
+ }
+
+ private static native long init();
+ private static native int setTime(long nativeData, @CurrentTimeMillisLong long millis);
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 481672e64d06..c4ebc9db6110 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -247,6 +247,7 @@ import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
import android.content.pm.PermissionInfo;
import android.content.pm.PermissionMethod;
+import android.content.pm.PermissionName;
import android.content.pm.ProcessInfo;
import android.content.pm.ProviderInfo;
import android.content.pm.ProviderInfoList;
@@ -5987,8 +5988,9 @@ public class ActivityManagerService extends IActivityManager.Stub
* provided non-{@code null} {@code permission} before. Otherwise calls into
* {@link ActivityManager#checkComponentPermission(String, int, int, boolean)}.
*/
+ @PackageManager.PermissionResult
@PermissionMethod
- public static int checkComponentPermission(String permission, int pid, int uid,
+ public static int checkComponentPermission(@PermissionName String permission, int pid, int uid,
int owningUid, boolean exported) {
if (pid == MY_PID) {
return PackageManager.PERMISSION_GRANTED;
@@ -6034,8 +6036,9 @@ public class ActivityManagerService extends IActivityManager.Stub
* This can be called with or without the global lock held.
*/
@Override
+ @PackageManager.PermissionResult
@PermissionMethod
- public int checkPermission(String permission, int pid, int uid) {
+ public int checkPermission(@PermissionName String permission, int pid, int uid) {
if (permission == null) {
return PackageManager.PERMISSION_DENIED;
}
@@ -6046,8 +6049,9 @@ public class ActivityManagerService extends IActivityManager.Stub
* Binder IPC calls go through the public entry point.
* This can be called with or without the global lock held.
*/
+ @PackageManager.PermissionResult
@PermissionMethod
- int checkCallingPermission(String permission) {
+ int checkCallingPermission(@PermissionName String permission) {
return checkPermission(permission,
Binder.getCallingPid(),
Binder.getCallingUid());
@@ -6057,7 +6061,7 @@ public class ActivityManagerService extends IActivityManager.Stub
* This can be called with or without the global lock held.
*/
@PermissionMethod
- void enforceCallingPermission(String permission, String func) {
+ void enforceCallingPermission(@PermissionName String permission, String func) {
if (checkCallingPermission(permission)
== PackageManager.PERMISSION_GRANTED) {
return;
@@ -6074,7 +6078,6 @@ public class ActivityManagerService extends IActivityManager.Stub
/**
* This can be called with or without the global lock held.
*/
- @PermissionMethod
private void enforceCallingHasAtLeastOnePermission(String func, String... permissions) {
for (String permission : permissions) {
if (checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) {
@@ -6093,7 +6096,8 @@ public class ActivityManagerService extends IActivityManager.Stub
/**
* This can be called with or without the global lock held.
*/
- void enforcePermission(String permission, int pid, int uid, String func) {
+ @PermissionMethod
+ void enforcePermission(@PermissionName String permission, int pid, int uid, String func) {
if (checkPermission(permission, pid, uid) == PackageManager.PERMISSION_GRANTED) {
return;
}
@@ -16399,23 +16403,17 @@ public class ActivityManagerService extends IActivityManager.Stub
return mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
}
- /**
- * Unlocks the given user.
- *
- * @param userId The ID of the user to unlock.
- * @param token No longer used. (This parameter cannot be removed because
- * this method is marked with UnsupportedAppUsage, so its
- * signature might not be safe to change.)
- * @param secret The secret needed to unlock the user's credential-encrypted
- * storage, or null if no secret is needed.
- * @param listener An optional progress listener.
- *
- * @return true if the user was successfully unlocked, otherwise false.
- */
+ /** @deprecated see the AIDL documentation {@inheritDoc} */
+ @Override
+ @Deprecated
+ public boolean unlockUser(@UserIdInt int userId, @Nullable byte[] token,
+ @Nullable byte[] secret, @Nullable IProgressListener listener) {
+ return mUserController.unlockUser(userId, listener);
+ }
+
@Override
- public boolean unlockUser(int userId, @Nullable byte[] token, @Nullable byte[] secret,
- @Nullable IProgressListener listener) {
- return mUserController.unlockUser(userId, secret, listener);
+ public boolean unlockUser2(@UserIdInt int userId, @Nullable IProgressListener listener) {
+ return mUserController.unlockUser(userId, listener);
}
@Override
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 10e2aae9a8d1..e4f947da868e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -2119,7 +2119,7 @@ final class ActivityManagerShellCommand extends ShellCommand {
return -1;
}
- boolean success = mInterface.unlockUser(userId, null, null, null);
+ boolean success = mInterface.unlockUser2(userId, null);
if (success) {
pw.println("Success: user unlocked");
} else {
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 82fb1e816888..226c63862226 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -654,7 +654,7 @@ class UserController implements Handler.Callback {
EventLog.writeEvent(EventLogTags.UC_FINISH_USER_UNLOCKING, userId);
logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_UNLOCKING_USER,
USER_LIFECYCLE_EVENT_STATE_BEGIN);
- // Only keep marching forward if user is actually unlocked
+ // If the user key hasn't been unlocked yet, we cannot proceed.
if (!StorageManager.isUserKeyUnlocked(userId)) return false;
synchronized (mLock) {
// Do not proceed if unexpected state or a stale user
@@ -1776,28 +1776,19 @@ class UserController implements Handler.Callback {
}
}
- boolean unlockUser(final @UserIdInt int userId, byte[] secret, IProgressListener listener) {
+ boolean unlockUser(@UserIdInt int userId, @Nullable IProgressListener listener) {
checkCallingPermission(INTERACT_ACROSS_USERS_FULL, "unlockUser");
EventLog.writeEvent(EventLogTags.UC_UNLOCK_USER, userId);
final long binderToken = Binder.clearCallingIdentity();
try {
- return unlockUserCleared(userId, secret, listener);
+ return maybeUnlockUser(userId, listener);
} finally {
Binder.restoreCallingIdentity(binderToken);
}
}
- /**
- * Attempt to unlock user without a secret. This typically succeeds when the
- * device doesn't have credential-encrypted storage, or when the
- * credential-encrypted storage isn't tied to a user-provided PIN or
- * pattern.
- */
- private boolean maybeUnlockUser(final @UserIdInt int userId) {
- return unlockUserCleared(userId, null, null);
- }
-
- private static void notifyFinished(@UserIdInt int userId, IProgressListener listener) {
+ private static void notifyFinished(@UserIdInt int userId,
+ @Nullable IProgressListener listener) {
if (listener == null) return;
try {
listener.onFinished(userId, null);
@@ -1805,8 +1796,18 @@ class UserController implements Handler.Callback {
}
}
- private boolean unlockUserCleared(final @UserIdInt int userId, byte[] secret,
- IProgressListener listener) {
+ private boolean maybeUnlockUser(@UserIdInt int userId) {
+ return maybeUnlockUser(userId, null);
+ }
+
+ /**
+ * Tries to unlock the given user.
+ * <p>
+ * This will succeed only if the user's CE storage key is already unlocked or if the user
+ * doesn't have a lockscreen credential set.
+ */
+ private boolean maybeUnlockUser(@UserIdInt int userId, @Nullable IProgressListener listener) {
+
// Delay user unlocking for headless system user mode until the system boot
// completes. When the system boot completes, the {@link #onBootCompleted()}
// method unlocks all started users for headless system user mode. This is done
@@ -1825,14 +1826,8 @@ class UserController implements Handler.Callback {
UserState uss;
if (!StorageManager.isUserKeyUnlocked(userId)) {
- final UserInfo userInfo = getUserInfo(userId);
- final IStorageManager storageManager = mInjector.getStorageManager();
- try {
- // We always want to unlock user storage, even user is not started yet
- storageManager.unlockUserKey(userId, userInfo.serialNumber, secret);
- } catch (RemoteException | RuntimeException e) {
- Slogf.w(TAG, "Failed to unlock: " + e.getMessage());
- }
+ // We always want to try to unlock the user key, even if the user is not started yet.
+ mLockPatternUtils.unlockUserKeyIfUnsecured(userId);
}
synchronized (mLock) {
// Register the given listener to watch for unlock progress
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthResult.java b/services/core/java/com/android/server/biometrics/sensors/AuthResult.java
new file mode 100644
index 000000000000..c0ebf6b8b634
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthResult.java
@@ -0,0 +1,40 @@
+/*
+ * 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.biometrics.sensors;
+
+import android.hardware.biometrics.BiometricManager;
+
+class AuthResult {
+ static final int FAILED = 0;
+ static final int LOCKED_OUT = 1;
+ static final int AUTHENTICATED = 2;
+ private final int mStatus;
+ private final int mBiometricStrength;
+
+ AuthResult(int status, @BiometricManager.Authenticators.Types int strength) {
+ mStatus = status;
+ mBiometricStrength = strength;
+ }
+
+ int getStatus() {
+ return mStatus;
+ }
+
+ int getBiometricStrength() {
+ return mBiometricStrength;
+ }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java
new file mode 100644
index 000000000000..6d00c3fc15ea
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java
@@ -0,0 +1,93 @@
+/*
+ * 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.biometrics.sensors;
+
+import android.hardware.biometrics.BiometricManager.Authenticators;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that takes in a series of authentication attempts (successes, failures, lockouts)
+ * across different biometric strengths (convenience, weak, strong) and returns a single AuthResult.
+ *
+ * The AuthResult will be the strongest biometric operation that occurred amongst all reported
+ * operations, and if multiple such operations exist, it will favor a successful authentication.
+ */
+class AuthResultCoordinator {
+
+ private static final String TAG = "AuthResultCoordinator";
+ private final List<AuthResult> mOperations;
+
+ AuthResultCoordinator() {
+ mOperations = new ArrayList<>();
+ }
+
+ /**
+ * Adds auth success for a given strength to the current operation list.
+ */
+ void authenticatedFor(@Authenticators.Types int strength) {
+ mOperations.add(new AuthResult(AuthResult.AUTHENTICATED, strength));
+ }
+
+ /**
+ * Adds auth ended for a given strength to the current operation list.
+ */
+ void authEndedFor(@Authenticators.Types int strength) {
+ mOperations.add(new AuthResult(AuthResult.FAILED, strength));
+ }
+
+ /**
+ * Adds a lock out of a given strength to the current operation list.
+ */
+ void lockedOutFor(@Authenticators.Types int strength) {
+ mOperations.add(new AuthResult(AuthResult.LOCKED_OUT, strength));
+ }
+
+ /**
+ * Obtains an auth result & strength from a current set of biometric operations.
+ */
+ AuthResult getResult() {
+ AuthResult result = new AuthResult(AuthResult.FAILED, Authenticators.BIOMETRIC_CONVENIENCE);
+ return mOperations.stream().filter(
+ (element) -> element.getStatus() != AuthResult.FAILED).reduce(result,
+ ((curr, next) -> {
+ int strengthCompare = curr.getBiometricStrength() - next.getBiometricStrength();
+ if (strengthCompare < 0) {
+ return curr;
+ } else if (strengthCompare == 0) {
+ // Equal level of strength, favor authentication.
+ if (curr.getStatus() == AuthResult.AUTHENTICATED) {
+ return curr;
+ } else {
+ // Either next is Authenticated, or it is not, either way return this
+ // one.
+ return next;
+ }
+ } else {
+ // curr is a weaker biometric
+ return next;
+ }
+ }));
+ }
+
+ void resetState() {
+ mOperations.clear();
+ }
+}
+
+
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
new file mode 100644
index 000000000000..13840ff389d9
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
@@ -0,0 +1,157 @@
+/*
+ * 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.biometrics.sensors;
+
+import android.hardware.biometrics.BiometricManager.Authenticators;
+import android.util.Slog;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Coordinates lockout counter enforcement for all types of biometric strengths across all users.
+ *
+ * This class is not thread-safe. In general, all calls to this class should be made on the same
+ * handler to ensure no collisions.
+ */
+class AuthSessionCoordinator implements AuthSessionListener {
+ private static final String TAG = "AuthSessionCoordinator";
+
+ private final Set<Integer> mAuthOperations;
+
+ private int mUserId;
+ private boolean mIsAuthenticating;
+ private AuthResultCoordinator mAuthResultCoordinator;
+ private MultiBiometricLockoutState mMultiBiometricLockoutState;
+
+ AuthSessionCoordinator() {
+ mAuthOperations = new HashSet<>();
+ mAuthResultCoordinator = new AuthResultCoordinator();
+ mMultiBiometricLockoutState = new MultiBiometricLockoutState();
+ }
+
+ /**
+ * A Call indicating that an auth session has started
+ */
+ void onAuthSessionStarted(int userId) {
+ mAuthOperations.clear();
+ mUserId = userId;
+ mIsAuthenticating = true;
+ mAuthResultCoordinator.resetState();
+ }
+
+ /**
+ * Ends the current auth session and updates the lockout state.
+ *
+ * This can happen two ways.
+ * 1. Manually calling this API
+ * 2. If authStartedFor() was called, and all authentication attempts finish.
+ */
+ void endAuthSession() {
+ if (mIsAuthenticating) {
+ mAuthOperations.clear();
+ AuthResult res =
+ mAuthResultCoordinator.getResult();
+ if (res.getStatus() == AuthResult.AUTHENTICATED) {
+ mMultiBiometricLockoutState.onUserUnlocked(mUserId, res.getBiometricStrength());
+ } else if (res.getStatus() == AuthResult.LOCKED_OUT) {
+ mMultiBiometricLockoutState.onUserLocked(mUserId, res.getBiometricStrength());
+ }
+ mAuthResultCoordinator.resetState();
+ mIsAuthenticating = false;
+ }
+ }
+
+ /**
+ * @return true if a user can authenticate with a given strength.
+ */
+ boolean getCanAuthFor(int userId, @Authenticators.Types int strength) {
+ return mMultiBiometricLockoutState.canUserAuthenticate(userId, strength);
+ }
+
+ @Override
+ public void authStartedFor(int userId, int sensorId) {
+ if (!mIsAuthenticating) {
+ onAuthSessionStarted(userId);
+ }
+
+ if (mAuthOperations.contains(sensorId)) {
+ Slog.e(TAG, "Error, authStartedFor(" + sensorId + ") without being finished");
+ return;
+ }
+
+ if (mUserId != userId) {
+ Slog.e(TAG, "Error authStartedFor(" + userId + ") Incorrect userId, expected" + mUserId
+ + ", ignoring...");
+ return;
+ }
+
+ mAuthOperations.add(sensorId);
+ }
+
+ @Override
+ public void authenticatedFor(int userId, @Authenticators.Types int biometricStrength,
+ int sensorId) {
+ mAuthResultCoordinator.authenticatedFor(biometricStrength);
+ attemptToFinish(userId, sensorId,
+ "authenticatedFor(userId=" + userId + ", biometricStrength=" + biometricStrength
+ + ", sensorId=" + sensorId + "");
+ }
+
+ @Override
+ public void lockedOutFor(int userId, @Authenticators.Types int biometricStrength,
+ int sensorId) {
+ mAuthResultCoordinator.lockedOutFor(biometricStrength);
+ attemptToFinish(userId, sensorId,
+ "lockOutFor(userId=" + userId + ", biometricStrength=" + biometricStrength
+ + ", sensorId=" + sensorId + "");
+ }
+
+ @Override
+ public void authEndedFor(int userId, @Authenticators.Types int biometricStrength,
+ int sensorId) {
+ mAuthResultCoordinator.authEndedFor(biometricStrength);
+ attemptToFinish(userId, sensorId,
+ "authEndedFor(userId=" + userId + " ,biometricStrength=" + biometricStrength
+ + ", sensorId=" + sensorId);
+ }
+
+ @Override
+ public void resetLockoutFor(int userId, @Authenticators.Types int biometricStrength) {
+ mMultiBiometricLockoutState.onUserUnlocked(userId, biometricStrength);
+ }
+
+ private void attemptToFinish(int userId, int sensorId, String description) {
+ boolean didFail = false;
+ if (!mAuthOperations.contains(sensorId)) {
+ Slog.e(TAG, "Error unable to find auth operation : " + description);
+ didFail = true;
+ }
+ if (userId != mUserId) {
+ Slog.e(TAG, "Error mismatched userId, expected=" + mUserId + " for " + description);
+ didFail = true;
+ }
+ if (didFail) {
+ return;
+ }
+ mAuthOperations.remove(sensorId);
+ if (mIsAuthenticating && mAuthOperations.isEmpty()) {
+ endAuthSession();
+ }
+ }
+
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java
new file mode 100644
index 000000000000..8b1f90af0234
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java
@@ -0,0 +1,49 @@
+/*
+ * 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.biometrics.sensors;
+
+import android.hardware.biometrics.BiometricManager.Authenticators;
+
+/**
+ * An interface that listens to authentication events.
+ */
+interface AuthSessionListener {
+ /**
+ * Indicates an auth operation has started for a given user and sensor.
+ */
+ void authStartedFor(int userId, int sensorId);
+
+ /**
+ * Indicates a successful authentication occurred for a sensor of a given strength.
+ */
+ void authenticatedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId);
+
+ /**
+ * Indicates authentication ended for a sensor of a given strength.
+ */
+ void authEndedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId);
+
+ /**
+ * Indicates a lockout occurred for a sensor of a given strength.
+ */
+ void lockedOutFor(int userId, @Authenticators.Types int biometricStrength, int sensorId);
+
+ /**
+ * Indicates that a reset lockout has happened for a given strength.
+ */
+ void resetLockoutFor(int uerId, @Authenticators.Types int biometricStrength);
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
new file mode 100644
index 000000000000..49dc817c7fd5
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
@@ -0,0 +1,117 @@
+/*
+ * 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.biometrics.sensors;
+
+import static android.hardware.biometrics.BiometricManager.Authenticators;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
+
+import android.util.ArrayMap;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class is used as a system to store the state of each
+ * {@link Authenticators.Types} status for every user.
+ */
+class MultiBiometricLockoutState {
+
+ private static final String TAG = "MultiBiometricLockoutState";
+ private static final Map<Integer, List<Integer>> PRECEDENCE;
+
+ static {
+ Map<Integer, List<Integer>> precedence = new ArrayMap<>();
+ precedence.put(Authenticators.BIOMETRIC_STRONG,
+ Arrays.asList(BIOMETRIC_STRONG, BIOMETRIC_WEAK, BIOMETRIC_CONVENIENCE));
+ precedence.put(BIOMETRIC_WEAK, Arrays.asList(BIOMETRIC_WEAK, BIOMETRIC_CONVENIENCE));
+ precedence.put(BIOMETRIC_CONVENIENCE, Arrays.asList(BIOMETRIC_CONVENIENCE));
+ PRECEDENCE = Collections.unmodifiableMap(precedence);
+ }
+
+ private final Map<Integer, Map<Integer, Boolean>> mCanUserAuthenticate;
+
+ @VisibleForTesting
+ MultiBiometricLockoutState() {
+ mCanUserAuthenticate = new HashMap<>();
+ }
+
+ private static Map<Integer, Boolean> createLockedOutMap() {
+ Map<Integer, Boolean> lockOutMap = new HashMap<>();
+ lockOutMap.put(BIOMETRIC_STRONG, false);
+ lockOutMap.put(BIOMETRIC_WEAK, false);
+ lockOutMap.put(BIOMETRIC_CONVENIENCE, false);
+ return lockOutMap;
+ }
+
+ private Map<Integer, Boolean> getAuthMapForUser(int userId) {
+ if (!mCanUserAuthenticate.containsKey(userId)) {
+ mCanUserAuthenticate.put(userId, createLockedOutMap());
+ }
+ return mCanUserAuthenticate.get(userId);
+ }
+
+ /**
+ * Indicates a {@link Authenticators} has been locked for userId.
+ *
+ * @param userId The user.
+ * @param strength The strength of biometric that is requested to be locked.
+ */
+ void onUserLocked(int userId, @Authenticators.Types int strength) {
+ Slog.d(TAG, "onUserLocked(userId=" + userId + ", strength=" + strength + ")");
+ Map<Integer, Boolean> canUserAuthState = getAuthMapForUser(userId);
+ for (int strengthToLockout : PRECEDENCE.get(strength)) {
+ canUserAuthState.put(strengthToLockout, false);
+ }
+ }
+
+ /**
+ * Indicates that a user has unlocked a {@link Authenticators}
+ *
+ * @param userId The user.
+ * @param strength The strength of biometric that is unlocked.
+ */
+ void onUserUnlocked(int userId, @Authenticators.Types int strength) {
+ Slog.d(TAG, "onUserUnlocked(userId=" + userId + ", strength=" + strength + ")");
+ Map<Integer, Boolean> canUserAuthState = getAuthMapForUser(userId);
+ for (int strengthToLockout : PRECEDENCE.get(strength)) {
+ canUserAuthState.put(strengthToLockout, true);
+ }
+ }
+
+ /**
+ * Indicates if a user can perform an authentication operation with a given
+ * {@link Authenticators.Types}
+ *
+ * @param userId The user.
+ * @param strength The strength of biometric that is requested to authenticate.
+ * @return If a user can authenticate with a given biometric of this strength.
+ */
+ boolean canUserAuthenticate(int userId, @Authenticators.Types int strength) {
+ final boolean canAuthenticate = getAuthMapForUser(userId).get(strength);
+ Slog.d(TAG, "canUserAuthenticate(userId=" + userId + ", strength=" + strength + ") ="
+ + canAuthenticate);
+ return canAuthenticate;
+ }
+}
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index 3e397468aa87..82436cc0890a 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -221,14 +221,12 @@ final class IInputMethodInvoker {
}
@AnyThread
- boolean updateEditorToolType(int toolType) {
+ void updateEditorToolType(@MotionEvent.ToolType int toolType) {
try {
mTarget.updateEditorToolType(toolType);
} catch (RemoteException e) {
logRemoteException(e);
- return false;
}
- return true;
}
@AnyThread
diff --git a/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java b/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java
index 5a0069ac8e06..789222eb08f1 100644
--- a/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java
+++ b/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java
@@ -1,18 +1,18 @@
/*
-** Copyright 2016, 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.
-*/
+ * Copyright (C) 2016 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.inputmethod;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
index 2160b652317a..dc2799e8e434 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
@@ -29,7 +29,7 @@ final class InputMethodDeviceConfigs {
private boolean mHideImeWhenNoEditorFocus;
private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener;
- public InputMethodDeviceConfigs() {
+ InputMethodDeviceConfigs() {
mDeviceConfigChangedListener = properties -> {
if (!DeviceConfig.NAMESPACE_INPUT_METHOD_MANAGER.equals(properties.getNamespace())) {
return;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 8a56afaa0d74..520a471224b2 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -69,7 +69,6 @@ import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
-import android.app.AppOpsManager;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
@@ -307,7 +306,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
final boolean mHasFeature;
private final ArrayMap<String, List<InputMethodSubtype>> mAdditionalSubtypeMap =
new ArrayMap<>();
- private final AppOpsManager mAppOpsManager;
private final UserManager mUserManager;
private final UserManagerInternal mUserManagerInternal;
private final InputMethodMenuController mMenuController;
@@ -1734,7 +1732,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
mInputMethodDeviceConfigs = new InputMethodDeviceConfigs();
mImeDisplayValidator = mWindowManagerInternal::getDisplayImePolicy;
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
- mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
mUserManager = mContext.getSystemService(UserManager.class);
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
mAccessibilityManager = AccessibilityManager.getInstance(context);
@@ -2520,7 +2517,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
null, null, null, selectedMethodId, getSequenceNumberLocked(), null, false);
}
- if (!InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, cs.mUid,
+ if (!InputMethodUtils.checkIfPackageBelongsToUid(mPackageManagerInternal, cs.mUid,
editorInfo.packageName)) {
Slog.e(TAG, "Rejecting this client as it reported an invalid package name."
+ " uid=" + cs.mUid + " package=" + editorInfo.packageName);
@@ -3957,7 +3954,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
return false;
}
if (getCurIntentLocked() != null && InputMethodUtils.checkIfPackageBelongsToUid(
- mAppOpsManager,
+ mPackageManagerInternal,
uid,
getCurIntentLocked().getComponent().getPackageName())) {
return true;
@@ -4156,6 +4153,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
if (UserHandle.getCallingUserId() != userId) {
mContext.enforceCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
+ final int callingUid = Binder.getCallingUid();
// By this IPC call, only a process which shares the same uid with the IME can add
// additional input method subtypes to the IME.
@@ -4176,7 +4174,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
if (mSettings.getCurrentUserId() == userId) {
if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded,
- mAdditionalSubtypeMap, mIPackageManager)) {
+ mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) {
return;
}
final long ident = Binder.clearCallingIdentity();
@@ -4188,14 +4186,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
return;
}
- final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
- final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
- userId, false);
+ final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
+ final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
+ methodList, DirectBootAwareness.AUTO);
+ final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
+ userId, false);
settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap,
- mIPackageManager);
+ mPackageManagerInternal, callingUid);
}
}
@@ -4208,8 +4209,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
final int callingUid = Binder.getCallingUid();
final ComponentName imeComponentName =
imeId != null ? ComponentName.unflattenFromString(imeId) : null;
- if (imeComponentName == null || !InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager,
- callingUid, imeComponentName.getPackageName())) {
+ if (imeComponentName == null || !InputMethodUtils.checkIfPackageBelongsToUid(
+ mPackageManagerInternal, callingUid, imeComponentName.getPackageName())) {
throw new SecurityException("Calling UID=" + callingUid + " does not belong to imeId="
+ imeId);
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index 11e6923aa75a..a25630ffefa1 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -71,7 +71,7 @@ final class InputMethodMenuController {
@Nullable
private InputMethodDialogWindowContext mDialogWindowContext;
- public InputMethodMenuController(InputMethodManagerService service) {
+ InputMethodMenuController(InputMethodManagerService service) {
mService = service;
mSettings = mService.mSettings;
mSwitchingController = mService.mSwitchingController;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index c57fe332867a..69b06613b298 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -20,16 +20,13 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserHandleAware;
import android.annotation.UserIdInt;
-import android.app.AppOpsManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
import android.content.res.Resources;
-import android.os.Binder;
import android.os.Build;
-import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
@@ -45,7 +42,6 @@ import android.view.textservice.SpellCheckerInfo;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.StartInputFlags;
-import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.textservices.TextServicesManagerInternal;
@@ -80,29 +76,6 @@ final class InputMethodUtils {
}
// ----------------------------------------------------------------------
- // Utilities for debug
- static String getApiCallStack() {
- String apiCallStack = "";
- try {
- throw new RuntimeException();
- } catch (RuntimeException e) {
- final StackTraceElement[] frames = e.getStackTrace();
- for (int j = 1; j < frames.length; ++j) {
- final String tempCallStack = frames[j].toString();
- if (TextUtils.isEmpty(apiCallStack)) {
- // Overwrite apiCallStack if it's empty
- apiCallStack = tempCallStack;
- } else if (tempCallStack.indexOf("Transact(") < 0) {
- // Overwrite apiCallStack if it's not a binder call
- apiCallStack = tempCallStack;
- } else {
- break;
- }
- }
- }
- return apiCallStack;
- }
- // ----------------------------------------------------------------------
static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
if (subtype == null) return true;
@@ -210,28 +183,27 @@ final class InputMethodUtils {
return subtype != null
? TextUtils.concat(subtype.getDisplayName(context,
imi.getPackageName(), imi.getServiceInfo().applicationInfo),
- (TextUtils.isEmpty(imiLabel) ?
- "" : " - " + imiLabel))
+ (TextUtils.isEmpty(imiLabel) ? "" : " - " + imiLabel))
: imiLabel;
}
/**
* Returns true if a package name belongs to a UID.
*
- * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
- * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
+ * <p>This is a simple wrapper of
+ * {@link PackageManagerInternal#getPackageUid(String, long, int)}.</p>
+ * @param packageManagerInternal the {@link PackageManagerInternal} object to be used for the
+ * validation.
* @param uid the UID to be validated.
* @param packageName the package name.
* @return {@code true} if the package name belongs to the UID.
*/
- static boolean checkIfPackageBelongsToUid(AppOpsManager appOpsManager,
+ static boolean checkIfPackageBelongsToUid(PackageManagerInternal packageManagerInternal,
int uid, String packageName) {
- try {
- appOpsManager.checkPackage(uid, packageName);
- return true;
- } catch (SecurityException e) {
- return false;
- }
+ // PackageManagerInternal#getPackageUid() doesn't check MATCH_INSTANT/MATCH_APEX as of
+ // writing. So setting 0 should be fine.
+ return packageManagerInternal.getPackageUid(packageName, 0 /* flags */,
+ UserHandle.getUserId(uid)) == uid;
}
/**
@@ -674,8 +646,8 @@ final class InputMethodUtils {
List<InputMethodSubtype> implicitlyEnabledSubtypes =
SubtypeUtils.getImplicitlyApplicableSubtypesLocked(mRes, imi);
if (implicitlyEnabledSubtypes != null) {
- final int N = implicitlyEnabledSubtypes.size();
- for (int i = 0; i < N; ++i) {
+ final int numSubtypes = implicitlyEnabledSubtypes.size();
+ for (int i = 0; i < numSubtypes; ++i) {
final InputMethodSubtype st = implicitlyEnabledSubtypes.get(i);
if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
return subtypeHashCode;
@@ -877,20 +849,13 @@ final class InputMethodUtils {
boolean setAdditionalInputMethodSubtypes(@NonNull String imeId,
@NonNull ArrayList<InputMethodSubtype> subtypes,
@NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
- @NonNull IPackageManager packageManager) {
+ @NonNull PackageManagerInternal packageManagerInternal, int callingUid) {
final InputMethodInfo imi = mMethodMap.get(imeId);
if (imi == null) {
return false;
}
- final String[] packageInfos;
- try {
- packageInfos = packageManager.getPackagesForUid(Binder.getCallingUid());
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to get package infos");
- return false;
- }
- if (ArrayUtils.find(packageInfos,
- packageInfo -> TextUtils.equals(packageInfo, imi.getPackageName())) == null) {
+ if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid,
+ imi.getPackageName())) {
return false;
}
diff --git a/services/core/java/com/android/server/inputmethod/LocaleUtils.java b/services/core/java/com/android/server/inputmethod/LocaleUtils.java
index 3d02b3af6bc1..f865e6010580 100644
--- a/services/core/java/com/android/server/inputmethod/LocaleUtils.java
+++ b/services/core/java/com/android/server/inputmethod/LocaleUtils.java
@@ -46,7 +46,7 @@ final class LocaleUtils {
* @param desired The locale preferred by user.
* @return A score based on the locale matching for the default subtype enabling.
*/
- @IntRange(from=1, to=3)
+ @IntRange(from = 1, to = 3)
private static byte calculateMatchingSubScore(@NonNull final ULocale supported,
@NonNull final ULocale desired) {
// Assuming supported/desired is fully expanded.
@@ -111,7 +111,7 @@ final class LocaleUtils {
* @return 1 if {@code left} is larger than {@code right}. -1 if {@code left} is less than
* {@code right}. 0 if {@code left} and {@code right} is equal.
*/
- @IntRange(from=-1, to=1)
+ @IntRange(from = -1, to = 1)
private static int compare(@NonNull byte[] left, @NonNull byte[] right) {
for (int i = 0; i < left.length; ++i) {
if (left[i] > right[i]) {
diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
index 7085868e4f61..f07539fa5eb2 100644
--- a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
+++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
@@ -68,14 +68,14 @@ final class SubtypeUtils {
if (locale == null) {
return false;
}
- final int N = imi.getSubtypeCount();
- for (int i = 0; i < N; ++i) {
+ final int numSubtypes = imi.getSubtypeCount();
+ for (int i = 0; i < numSubtypes; ++i) {
final InputMethodSubtype subtype = imi.getSubtypeAt(i);
if (checkCountry) {
final Locale subtypeLocale = subtype.getLocaleObject();
- if (subtypeLocale == null ||
- !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
- !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
+ if (subtypeLocale == null
+ || !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())
+ || !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
continue;
}
} else {
@@ -260,8 +260,8 @@ final class SubtypeUtils {
boolean partialMatchFound = false;
InputMethodSubtype applicableSubtype = null;
InputMethodSubtype firstMatchedModeSubtype = null;
- final int N = subtypes.size();
- for (int i = 0; i < N; ++i) {
+ final int numSubtypes = subtypes.size();
+ for (int i = 0; i < numSubtypes; ++i) {
InputMethodSubtype subtype = subtypes.get(i);
final String subtypeLocale = subtype.getLocale();
final String subtypeLanguage = LocaleUtils.getLanguageFromLocaleString(subtypeLocale);
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 533d1b0c7034..58d677c31606 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -113,6 +113,7 @@ import android.util.EventLog;
import android.util.LongSparseArray;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseIntArray;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
@@ -141,6 +142,7 @@ import com.android.server.locksettings.SyntheticPasswordManager.SyntheticPasswor
import com.android.server.locksettings.SyntheticPasswordManager.TokenType;
import com.android.server.locksettings.recoverablekeystore.RecoverableKeyStoreManager;
import com.android.server.pm.UserManagerInternal;
+import com.android.server.utils.Slogf;
import com.android.server.wm.WindowManagerInternal;
import libcore.util.HexEncoding;
@@ -243,6 +245,17 @@ public class LockSettingsService extends ILockSettings.Stub {
private final RebootEscrowManager mRebootEscrowManager;
+ // Locking order is mUserCreationAndRemovalLock -> mSpManager.
+ private final Object mUserCreationAndRemovalLock = new Object();
+ // These two arrays are only used at boot time. To save memory, they are set to null when
+ // PHASE_BOOT_COMPLETED is reached.
+ @GuardedBy("mUserCreationAndRemovalLock")
+ private SparseIntArray mEarlyCreatedUsers = new SparseIntArray();
+ @GuardedBy("mUserCreationAndRemovalLock")
+ private SparseIntArray mEarlyRemovedUsers = new SparseIntArray();
+ @GuardedBy("mUserCreationAndRemovalLock")
+ private boolean mBootComplete;
+
// Current password metric for all users on the device. Updated when user unlocks
// the device or changes password. Removed when user is stopped.
@GuardedBy("this")
@@ -283,9 +296,16 @@ public class LockSettingsService extends ILockSettings.Stub {
@Override
public void onBootPhase(int phase) {
super.onBootPhase(phase);
- if (phase == PHASE_ACTIVITY_MANAGER_READY) {
- mLockSettingsService.migrateOldDataAfterSystemReady();
- mLockSettingsService.loadEscrowData();
+ switch (phase) {
+ case PHASE_ACTIVITY_MANAGER_READY:
+ mLockSettingsService.migrateOldDataAfterSystemReady();
+ mLockSettingsService.loadEscrowData();
+ break;
+ case PHASE_BOOT_COMPLETED:
+ mLockSettingsService.bootCompleted();
+ break;
+ default:
+ break;
}
}
@@ -577,7 +597,6 @@ public class LockSettingsService extends ILockSettings.Stub {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_ADDED);
filter.addAction(Intent.ACTION_USER_STARTING);
- filter.addAction(Intent.ACTION_USER_REMOVED);
injector.getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
null, null);
@@ -720,28 +739,32 @@ public class LockSettingsService extends ILockSettings.Stub {
}
/**
- * Clean up states associated with the given user, in case the userId is reused but LSS didn't
- * get a chance to do cleanup previously during ACTION_USER_REMOVED.
- *
- * Internally, LSS stores serial number for each user and check it against the current user's
- * serial number to determine if the userId is reused and invoke cleanup code.
+ * Removes the LSS state for the given userId if the userId was reused without its LSS state
+ * being fully removed.
+ * <p>
+ * This is primarily needed for users that were removed by Android 13 or earlier, which didn't
+ * guarantee removal of LSS state as it relied on the {@code ACTION_USER_REMOVED} intent. It is
+ * also needed because {@link #removeUser()} delays requests to remove LSS state until the
+ * {@code PHASE_BOOT_COMPLETED} boot phase, so they can be lost.
+ * <p>
+ * Stale state is detected by checking whether the user serial number changed. This works
+ * because user serial numbers are never reused.
*/
- private void cleanupDataForReusedUserIdIfNecessary(int userId) {
+ private void removeStateForReusedUserIdIfNecessary(@UserIdInt int userId, int serialNumber) {
if (userId == UserHandle.USER_SYSTEM) {
// Short circuit as we never clean up user 0.
return;
}
- // Serial number is never reusued, so we can use it as a distinguisher for user Id reuse.
- int serialNumber = mUserManager.getUserSerialNumber(userId);
-
int storedSerialNumber = mStorage.getInt(USER_SERIAL_NUMBER_KEY, -1, userId);
if (storedSerialNumber != serialNumber) {
// If LockSettingsStorage does not have a copy of the serial number, it could be either
// this is a user created before the serial number recording logic is introduced, or
// the user does not exist or was removed and cleaned up properly. In either case, don't
- // invoke removeUser().
+ // invoke removeUserState().
if (storedSerialNumber != -1) {
- removeUser(userId, /* unknownUser */ true);
+ Slogf.i(TAG, "Removing stale state for reused userId %d (serial %d => %d)", userId,
+ storedSerialNumber, serialNumber);
+ removeUserState(userId);
}
mStorage.setInt(USER_SERIAL_NUMBER_KEY, serialNumber, userId);
}
@@ -771,7 +794,6 @@ public class LockSettingsService extends ILockSettings.Stub {
mHandler.post(new Runnable() {
@Override
public void run() {
- cleanupDataForReusedUserIdIfNecessary(userId);
ensureProfileKeystoreUnlocked(userId);
// Hide notification first, as tie managed profile lock takes time
hideEncryptionNotification(new UserHandle(userId));
@@ -779,38 +801,10 @@ public class LockSettingsService extends ILockSettings.Stub {
if (isCredentialSharableWithParent(userId)) {
tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
}
-
- // If the user doesn't have a credential, try and derive their secret for the
- // AuthSecret HAL. The secret will have been enrolled if the user previously set a
- // credential and still needs to be passed to the HAL once that credential is
- // removed.
- if (mUserManager.getUserInfo(userId).isPrimary() && !isUserSecure(userId)) {
- tryDeriveVendorAuthSecretForUnsecuredPrimaryUser(userId);
- }
}
});
}
- private void tryDeriveVendorAuthSecretForUnsecuredPrimaryUser(@UserIdInt int userId) {
- synchronized (mSpManager) {
- // If there is no SP, then there is no vendor auth secret.
- if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
- return;
- }
-
- final long protectorId = getCurrentLskfBasedProtectorId(userId);
- AuthenticationResult result =
- mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
- LockscreenCredential.createNone(), userId, null);
- if (result.syntheticPassword != null) {
- Slog.i(TAG, "Unwrapped SP for unsecured primary user " + userId);
- onSyntheticPasswordKnown(userId, result.syntheticPassword);
- } else {
- Slog.e(TAG, "Failed to unwrap SP for unsecured primary user " + userId);
- }
- }
- }
-
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -821,11 +815,6 @@ public class LockSettingsService extends ILockSettings.Stub {
} else if (Intent.ACTION_USER_STARTING.equals(intent.getAction())) {
final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
mStorage.prefetchUser(userHandle);
- } else if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
- final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
- if (userHandle > 0) {
- removeUser(userHandle, /* unknownUser= */ false);
- }
}
}
};
@@ -937,6 +926,79 @@ public class LockSettingsService extends ILockSettings.Stub {
return success;
}
+ private void bootCompleted() {
+ synchronized (mUserCreationAndRemovalLock) {
+ // Handle delayed calls to LSS.removeUser() and LSS.createNewUser().
+ for (int i = 0; i < mEarlyRemovedUsers.size(); i++) {
+ int userId = mEarlyRemovedUsers.keyAt(i);
+ Slogf.i(TAG, "Removing locksettings state for removed user %d now that boot "
+ + "is complete", userId);
+ removeUserState(userId);
+ }
+ mEarlyRemovedUsers = null; // no longer needed
+ for (int i = 0; i < mEarlyCreatedUsers.size(); i++) {
+ int userId = mEarlyCreatedUsers.keyAt(i);
+ int serialNumber = mEarlyCreatedUsers.valueAt(i);
+
+ removeStateForReusedUserIdIfNecessary(userId, serialNumber);
+ synchronized (mSpManager) {
+ if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
+ Slogf.i(TAG, "Creating locksettings state for user %d now that boot "
+ + "is complete", userId);
+ initializeSyntheticPasswordLocked(userId);
+ }
+ }
+ }
+ mEarlyCreatedUsers = null; // no longer needed
+
+ // Also do a one-time migration of all users to SP-based credentials with the CE key
+ // encrypted by the SP. This is needed for the system user on the first boot of a
+ // device, as the system user is special and never goes through the user creation flow
+ // that other users do. It is also needed for existing users on a device upgraded from
+ // Android 13 or earlier, where users with no LSKF didn't necessarily have an SP, and if
+ // they did have an SP then their CE key wasn't encrypted by it.
+ //
+ // If this gets interrupted (e.g. by the device powering off), there shouldn't be a
+ // problem since this will run again on the next boot, and setUserKeyProtection() is
+ // okay with the key being already protected by the given secret.
+ if (getString("migrated_all_users_to_sp_and_bound_ce", null, 0) == null) {
+ for (UserInfo user : mUserManager.getAliveUsers()) {
+ removeStateForReusedUserIdIfNecessary(user.id, user.serialNumber);
+ synchronized (mSpManager) {
+ migrateUserToSpWithBoundCeKeyLocked(user.id);
+ }
+ }
+ setString("migrated_all_users_to_sp_and_bound_ce", "true", 0);
+ }
+
+ mBootComplete = true;
+ }
+ }
+
+ @GuardedBy("mSpManager")
+ private void migrateUserToSpWithBoundCeKeyLocked(@UserIdInt int userId) {
+ if (isUserSecure(userId)) {
+ Slogf.d(TAG, "User %d is secured; no migration needed", userId);
+ return;
+ }
+ long protectorId = getCurrentLskfBasedProtectorId(userId);
+ if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+ Slogf.i(TAG, "Migrating unsecured user %d to SP-based credential", userId);
+ initializeSyntheticPasswordLocked(userId);
+ } else {
+ Slogf.i(TAG, "Existing unsecured user %d has a synthetic password; re-encrypting CE " +
+ "key with it", userId);
+ AuthenticationResult result = mSpManager.unlockLskfBasedProtector(
+ getGateKeeperService(), protectorId, LockscreenCredential.createNone(), userId,
+ null);
+ if (result.syntheticPassword == null) {
+ Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
+ return;
+ }
+ setUserKeyProtection(userId, result.syntheticPassword.deriveFileBasedEncryptionKey());
+ }
+ }
+
/**
* Returns the lowest password quality that still presents the same UI for entering it.
*
@@ -1269,9 +1331,8 @@ public class LockSettingsService extends ILockSettings.Stub {
* can end up calling into other system services to process user unlock request (via
* {@link com.android.server.SystemServiceManager#unlockUser} </em>
*/
- private void unlockUser(int userId, byte[] secret) {
- Slog.i(TAG, "Unlocking user " + userId + " with secret only, length "
- + (secret != null ? secret.length : 0));
+ private void unlockUser(@UserIdInt int userId) {
+ Slogf.i(TAG, "Unlocking user %d", userId);
// TODO: make this method fully async so we can update UI with progress strings
final boolean alreadyUnlocked = mUserManager.isUserUnlockingOrUnlocked(userId);
final CountDownLatch latch = new CountDownLatch(1);
@@ -1294,7 +1355,7 @@ public class LockSettingsService extends ILockSettings.Stub {
};
try {
- mActivityManager.unlockUser(userId, null, secret, listener);
+ mActivityManager.unlockUser2(userId, listener);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
@@ -1587,6 +1648,7 @@ public class LockSettingsService extends ILockSettings.Stub {
if (!savedCredential.isNone()) {
throw new IllegalStateException("Saved credential given, but user has no SP");
}
+ // TODO(b/232452368): this case is only needed by unit tests now; remove it.
initializeSyntheticPasswordLocked(userId);
} else if (savedCredential.isNone() && isProfileWithUnifiedLock(userId)) {
// get credential from keystore when profile has unified lock
@@ -1897,19 +1959,12 @@ public class LockSettingsService extends ILockSettings.Stub {
mStorage.writeChildProfileLock(userId, ArrayUtils.concat(iv, ciphertext));
}
- private void setUserKeyProtection(int userId, byte[] key) {
- if (DEBUG) Slog.d(TAG, "setUserKeyProtection: user=" + userId);
- addUserKeyAuth(userId, key);
- }
-
- private void clearUserKeyProtection(int userId, byte[] secret) {
- if (DEBUG) Slog.d(TAG, "clearUserKeyProtection user=" + userId);
- final UserInfo userInfo = mUserManager.getUserInfo(userId);
+ private void setUserKeyProtection(@UserIdInt int userId, byte[] secret) {
final long callingId = Binder.clearCallingIdentity();
try {
- mStorageManager.clearUserKeyAuth(userId, userInfo.serialNumber, secret);
+ mStorageManager.setUserKeyProtection(userId, secret);
} catch (RemoteException e) {
- throw new IllegalStateException("clearUserKeyAuth failed user=" + userId);
+ throw new IllegalStateException("Failed to protect CE key for user " + userId, e);
} finally {
Binder.restoreCallingIdentity(callingId);
}
@@ -1924,40 +1979,51 @@ public class LockSettingsService extends ILockSettings.Stub {
}
}
- /** Unlock file-based encryption */
- private void unlockUserKey(int userId, byte[] secret) {
- final UserInfo userInfo = mUserManager.getUserInfo(userId);
- try {
- mStorageManager.unlockUserKey(userId, userInfo.serialNumber, secret);
- } catch (RemoteException e) {
- throw new IllegalStateException("Failed to unlock user key " + userId, e);
-
+ /**
+ * Unlocks the user's CE (credential-encrypted) storage if it's not already unlocked.
+ * <p>
+ * This method doesn't throw exceptions because it is called opportunistically whenever a user
+ * is started. Whether it worked or not can be detected by whether the key got unlocked or not.
+ */
+ private void unlockUserKey(@UserIdInt int userId, SyntheticPassword sp) {
+ if (isUserKeyUnlocked(userId)) {
+ Slogf.d(TAG, "CE storage for user %d is already unlocked", userId);
+ return;
}
- }
-
- private void addUserKeyAuth(int userId, byte[] secret) {
final UserInfo userInfo = mUserManager.getUserInfo(userId);
- final long callingId = Binder.clearCallingIdentity();
+ final String userType = isUserSecure(userId) ? "secured" : "unsecured";
+ final byte[] secret = sp.deriveFileBasedEncryptionKey();
try {
- mStorageManager.addUserKeyAuth(userId, userInfo.serialNumber, secret);
+ mStorageManager.unlockUserKey(userId, userInfo.serialNumber, secret);
+ Slogf.i(TAG, "Unlocked CE storage for %s user %d", userType, userId);
} catch (RemoteException e) {
- throw new IllegalStateException("Failed to add new key to vold " + userId, e);
+ Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId);
} finally {
- Binder.restoreCallingIdentity(callingId);
+ Arrays.fill(secret, (byte) 0);
}
}
- private void fixateNewestUserKeyAuth(int userId) {
- if (DEBUG) Slog.d(TAG, "fixateNewestUserKeyAuth: user=" + userId);
- final long callingId = Binder.clearCallingIdentity();
- try {
- mStorageManager.fixateNewestUserKeyAuth(userId);
- } catch (RemoteException e) {
- // OK to ignore the exception as vold would just accept both old and new
- // keys if this call fails, and will fix itself during the next boot
- Slog.w(TAG, "fixateNewestUserKeyAuth failed", e);
- } finally {
- Binder.restoreCallingIdentity(callingId);
+ private void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
+ synchronized (mSpManager) {
+ if (isUserKeyUnlocked(userId)) {
+ Slogf.d(TAG, "CE storage for user %d is already unlocked", userId);
+ return;
+ }
+ if (isUserSecure(userId)) {
+ Slogf.d(TAG, "Not unlocking CE storage for user %d yet because user is secured",
+ userId);
+ return;
+ }
+ Slogf.i(TAG, "Unwrapping synthetic password for unsecured user %d", userId);
+ AuthenticationResult result = mSpManager.unlockLskfBasedProtector(
+ getGateKeeperService(), getCurrentLskfBasedProtectorId(userId),
+ LockscreenCredential.createNone(), userId, null);
+ if (result.syntheticPassword == null) {
+ Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
+ return;
+ }
+ onSyntheticPasswordKnown(userId, result.syntheticPassword);
+ unlockUserKey(userId, result.syntheticPassword);
}
}
@@ -2228,8 +2294,50 @@ public class LockSettingsService extends ILockSettings.Stub {
});
}
- private void removeUser(int userId, boolean unknownUser) {
- Slog.i(TAG, "RemoveUser: " + userId);
+ private void createNewUser(@UserIdInt int userId, int userSerialNumber) {
+ synchronized (mUserCreationAndRemovalLock) {
+ // Before PHASE_BOOT_COMPLETED, don't actually create the synthetic password yet, but
+ // rather automatically delay it to later. We do this because protecting the synthetic
+ // password requires the Weaver HAL if the device supports it, and some devices don't
+ // make Weaver available until fairly late in the boot process. This logic ensures a
+ // consistent flow across all devices, regardless of their Weaver implementation.
+ if (!mBootComplete) {
+ Slogf.i(TAG, "Delaying locksettings state creation for user %d until boot complete",
+ userId);
+ mEarlyCreatedUsers.put(userId, userSerialNumber);
+ mEarlyRemovedUsers.delete(userId);
+ return;
+ }
+ removeStateForReusedUserIdIfNecessary(userId, userSerialNumber);
+ synchronized (mSpManager) {
+ initializeSyntheticPasswordLocked(userId);
+ }
+ }
+ }
+
+ private void removeUser(@UserIdInt int userId) {
+ synchronized (mUserCreationAndRemovalLock) {
+ // Before PHASE_BOOT_COMPLETED, don't actually remove the LSS state yet, but rather
+ // automatically delay it to later. We do this because deleting synthetic password
+ // protectors requires the Weaver HAL if the device supports it, and some devices don't
+ // make Weaver available until fairly late in the boot process. This logic ensures a
+ // consistent flow across all devices, regardless of their Weaver implementation.
+ if (!mBootComplete) {
+ Slogf.i(TAG, "Delaying locksettings state removal for user %d until boot complete",
+ userId);
+ if (mEarlyCreatedUsers.indexOfKey(userId) >= 0) {
+ mEarlyCreatedUsers.delete(userId);
+ } else {
+ mEarlyRemovedUsers.put(userId, -1 /* unused */);
+ }
+ return;
+ }
+ Slogf.i(TAG, "Removing state for user %d", userId);
+ removeUserState(userId);
+ }
+ }
+
+ private void removeUserState(@UserIdInt int userId) {
removeBiometricsForUser(userId);
mSpManager.removeUser(getGateKeeperService(), userId);
mStrongAuth.removeUser(userId);
@@ -2238,11 +2346,9 @@ public class LockSettingsService extends ILockSettings.Stub {
mManagedProfilePasswordCache.removePassword(userId);
gateKeeperClearSecureUserId(userId);
- if (unknownUser || isCredentialSharableWithParent(userId)) {
- removeKeystoreProfileKey(userId);
- }
- // Clean up storage last, this is to ensure that cleanupDataForReusedUserIdIfNecessary()
- // can make the assumption that no USER_SERIAL_NUMBER_KEY means user is fully removed.
+ removeKeystoreProfileKey(userId);
+ // Clean up storage last, so that removeStateForReusedUserIdIfNecessary() can assume that no
+ // USER_SERIAL_NUMBER_KEY means user is fully removed.
mStorage.removeUser(userId);
}
@@ -2497,8 +2603,11 @@ public class LockSettingsService extends ILockSettings.Stub {
}
private void callToAuthSecretIfNeeded(@UserIdInt int userId, SyntheticPassword sp) {
- // Pass the primary user's auth secret to the HAL
- if (mAuthSecretService != null && mUserManager.getUserInfo(userId).isPrimary()) {
+ // If the given user is the primary user, pass the auth secret to the HAL. Only the system
+ // user can be primary. Check for the system user ID before calling getUserInfo(), as other
+ // users may still be under construction.
+ if (mAuthSecretService != null && userId == UserHandle.USER_SYSTEM &&
+ mUserManager.getUserInfo(userId).isPrimary()) {
try {
final byte[] rawSecret = sp.deriveVendorAuthSecret();
final ArrayList<Byte> secret = new ArrayList<>(rawSecret.length);
@@ -2513,9 +2622,13 @@ public class LockSettingsService extends ILockSettings.Stub {
}
/**
- * Creates the synthetic password (SP) for the given user and protects it with an empty LSKF.
- * This is called just once in the lifetime of the user: the first time a nonempty LSKF is set,
- * or when an escrow token is activated on a device with an empty LSKF.
+ * Creates the synthetic password (SP) for the given user, protects it with an empty LSKF, and
+ * protects the user's CE key with a key derived from the SP.
+ * <p>
+ * This is called just once in the lifetime of the user: at user creation time (possibly delayed
+ * until {@code PHASE_BOOT_COMPLETED} to ensure that the Weaver HAL is available if the device
+ * supports it), or when upgrading from Android 13 or earlier where users with no LSKF didn't
+ * necessarily have an SP.
*/
@GuardedBy("mSpManager")
@VisibleForTesting
@@ -2529,6 +2642,7 @@ public class LockSettingsService extends ILockSettings.Stub {
final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(),
LockscreenCredential.createNone(), sp, userId);
setCurrentLskfBasedProtectorId(protectorId, userId);
+ setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
onSyntheticPasswordKnown(userId, sp);
return sp;
}
@@ -2601,11 +2715,10 @@ public class LockSettingsService extends ILockSettings.Stub {
unlockKeystore(sp.deriveKeyStorePassword(), userId);
- {
- final byte[] secret = sp.deriveFileBasedEncryptionKey();
- unlockUser(userId, secret);
- Arrays.fill(secret, (byte) 0);
- }
+ unlockUserKey(userId, sp);
+
+ unlockUser(userId);
+
activateEscrowTokens(sp, userId);
if (isProfileWithSeparatedLock(userId)) {
@@ -2626,9 +2739,9 @@ public class LockSettingsService extends ILockSettings.Stub {
* be empty) and replacing the old LSKF-based protector with it. The SP itself is not changed.
*
* Also maintains the invariants described in {@link SyntheticPasswordManager} by
- * setting/clearing the protection (by the SP) on the user's file-based encryption key and
- * auth-bound Keystore keys when the LSKF is added/removed, respectively. If the new LSKF is
- * nonempty, then the Gatekeeper auth token is also refreshed.
+ * setting/clearing the protection (by the SP) on the user's auth-bound Keystore keys when the
+ * LSKF is added/removed, respectively. If the new LSKF is nonempty, then the Gatekeeper auth
+ * token is also refreshed.
*/
@GuardedBy("mSpManager")
private long setLockCredentialWithSpLocked(LockscreenCredential credential,
@@ -2648,8 +2761,6 @@ public class LockSettingsService extends ILockSettings.Stub {
} else {
mSpManager.newSidForUser(getGateKeeperService(), sp, userId);
mSpManager.verifyChallenge(getGateKeeperService(), sp, 0L, userId);
- setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
- fixateNewestUserKeyAuth(userId);
setKeystorePassword(sp.deriveKeyStorePassword(), userId);
}
} else {
@@ -2659,9 +2770,7 @@ public class LockSettingsService extends ILockSettings.Stub {
mSpManager.clearSidForUser(userId);
gateKeeperClearSecureUserId(userId);
- unlockUserKey(userId, sp.deriveFileBasedEncryptionKey());
- clearUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
- fixateNewestUserKeyAuth(userId);
+ unlockUserKey(userId, sp);
unlockKeystore(sp.deriveKeyStorePassword(), userId);
setKeystorePassword(null, userId);
removeBiometricsForUser(userId);
@@ -2804,6 +2913,7 @@ public class LockSettingsService extends ILockSettings.Stub {
if (!isUserSecure(userId)) {
long protectorId = getCurrentLskfBasedProtectorId(userId);
if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+ // TODO(b/232452368): this case is only needed by unit tests now; remove it.
sp = initializeSyntheticPasswordLocked(userId);
} else {
sp = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
@@ -2888,7 +2998,7 @@ public class LockSettingsService extends ILockSettings.Stub {
// If clearing credential, unlock the user manually in order to progress user start
// Call unlockUser() on a handler thread so no lock is held (either by LSS or by
// the caller like DPMS), otherwise it can lead to deadlock.
- mHandler.post(() -> unlockUser(userId, null));
+ mHandler.post(() -> unlockUser(userId));
}
notifyPasswordChanged(credential, userId);
notifySeparateProfileChallengeChanged(userId);
@@ -3041,6 +3151,9 @@ public class LockSettingsService extends ILockSettings.Stub {
pw.decreaseIndent();
pw.println("PasswordHandleCount: " + mGatekeeperPasswords.size());
+ synchronized (mUserCreationAndRemovalLock) {
+ pw.println("BootComplete: " + mBootComplete);
+ }
}
private void dumpKeystoreKeys(IndentingPrintWriter pw) {
@@ -3197,6 +3310,21 @@ public class LockSettingsService extends ILockSettings.Stub {
private final class LocalService extends LockSettingsInternal {
@Override
+ public void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
+ LockSettingsService.this.unlockUserKeyIfUnsecured(userId);
+ }
+
+ @Override
+ public void createNewUser(@UserIdInt int userId, int userSerialNumber) {
+ LockSettingsService.this.createNewUser(userId, userSerialNumber);
+ }
+
+ @Override
+ public void removeUser(@UserIdInt int userId) {
+ LockSettingsService.this.removeUser(userId);
+ }
+
+ @Override
public long addEscrowToken(byte[] token, int userId,
EscrowTokenStateChangeCallback callback) {
return LockSettingsService.this.addEscrowToken(token, TOKEN_TYPE_STRONG, userId,
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index f1afb96866eb..66bdadb185c1 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -49,6 +49,7 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse;
import com.android.server.locksettings.LockSettingsStorage.PersistentData;
+import com.android.server.utils.Slogf;
import libcore.util.HexEncoding;
@@ -79,11 +80,12 @@ import java.util.Set;
* LockscreenCredential. The LSKF may be empty (none). There may be escrow token-based
* protectors as well, only for specific use cases such as enterprise-managed users.
*
- * - While the user's LSKF is nonempty, the SP protects the user's CE (credential encrypted)
- * storage and auth-bound Keystore keys: the user's CE key is encrypted by an SP-derived secret,
- * and the user's Keystore and Gatekeeper passwords are other SP-derived secrets. However, while
- * the user's LSKF is empty, these protections are cleared; this is needed to invalidate the
- * auth-bound keys and make UserController.unlockUser() work with an empty secret.
+ * - The user's credential-encrypted storage is always protected by the SP.
+ *
+ * - The user's auth-bound Keystore keys are protected by the SP, but only while an LSKF is set.
+ * This works by setting the user's Keystore and Gatekeeper passwords to SP-derived secrets, but
+ * only while an LSKF is set. When the LSKF is removed, these passwords are cleared,
+ * invalidating the user's auth-bound keys.
*
* Files stored on disk for each user:
* For the SP itself, stored under NULL_PROTECTOR_ID:
@@ -1026,6 +1028,14 @@ public class SyntheticPasswordManager {
long protectorId, @NonNull LockscreenCredential credential, int userId,
ICheckCredentialProgressCallback progressCallback) {
AuthenticationResult result = new AuthenticationResult();
+
+ if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+ // This should never happen, due to the migration done in LSS.bootCompleted().
+ Slogf.wtf(TAG, "Synthetic password not found for user %d", userId);
+ result.gkResponse = VerifyCredentialResponse.ERROR;
+ return result;
+ }
+
PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, protectorId,
userId));
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 0c601bfde05a..890c89152a7c 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1962,10 +1962,15 @@ class ShortcutPackage extends ShortcutPackageItem {
continue;
case TAG_SHORTCUT:
- final ShortcutInfo si = parseShortcut(parser, packageName,
- shortcutUser.getUserId(), fromBackup);
- // Don't use addShortcut(), we don't need to save the icon.
- ret.mShortcuts.put(si.getId(), si);
+ try {
+ final ShortcutInfo si = parseShortcut(parser, packageName,
+ shortcutUser.getUserId(), fromBackup);
+ // Don't use addShortcut(), we don't need to save the icon.
+ ret.mShortcuts.put(si.getId(), si);
+ } catch (Exception e) {
+ // b/246540168 malformed shortcuts should be ignored
+ Slog.e(TAG, "Failed parsing shortcut.", e);
+ }
continue;
case TAG_SHARE_TARGET:
ret.mShareTargets.add(ShareTargetInfo.loadFromXml(parser));
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index c39cbae0ebf2..0a89d131eda2 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -88,8 +88,6 @@ import android.os.UserManager.QuietModeFlag;
import android.os.storage.StorageManager;
import android.os.storage.StorageManagerInternal;
import android.provider.Settings;
-import android.security.GateKeeper;
-import android.service.gatekeeper.IGateKeeperService;
import android.service.voice.VoiceInteractionManagerInternal;
import android.stats.devicepolicy.DevicePolicyEnums;
import android.text.TextUtils;
@@ -4664,6 +4662,10 @@ public class UserManagerService extends IUserManager.Stub {
StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
t.traceEnd();
+ t.traceBegin("LSS.createNewUser");
+ mLockPatternUtils.createNewUser(userId, userInfo.serialNumber);
+ t.traceEnd();
+
final Set<String> userTypeInstallablePackages =
mSystemPackageInstaller.getInstallablePackagesForUserType(userType);
t.traceBegin("PM.createNewUser");
@@ -5500,15 +5502,8 @@ public class UserManagerService extends IUserManager.Stub {
Slog.i(LOG_TAG, "Destroying key for user " + userId + " failed, continuing anyway", e);
}
- // Cleanup gatekeeper secure user id
- try {
- final IGateKeeperService gk = GateKeeper.getService();
- if (gk != null) {
- gk.clearSecureUserId(userId);
- }
- } catch (Exception ex) {
- Slog.w(LOG_TAG, "unable to clear GK secure user id");
- }
+ // Cleanup lock settings
+ mLockPatternUtils.removeUser(userId);
// Cleanup package manager settings
mPm.cleanUpUser(this, userId);
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 2d22b8fc3272..f9352cb2b30f 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -5316,7 +5316,7 @@ public final class PowerManagerService extends SystemService
private final class SuspendBlockerImpl implements SuspendBlocker {
private static final String UNKNOWN_ID = "unknown";
private final String mName;
- private final String mTraceName;
+ private final int mNameHash;
private int mReferenceCount;
// Maps suspend blocker IDs to a list (LongArray) of open acquisitions for the suspend
@@ -5325,7 +5325,7 @@ public final class PowerManagerService extends SystemService
public SuspendBlockerImpl(String name) {
mName = name;
- mTraceName = "SuspendBlocker (" + name + ")";
+ mNameHash = mName.hashCode();
}
@Override
@@ -5336,7 +5336,8 @@ public final class PowerManagerService extends SystemService
+ "\" was finalized without being released!");
mReferenceCount = 0;
mNativeWrapper.nativeReleaseSuspendBlocker(mName);
- Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+ "SuspendBlockers", mNameHash);
}
} finally {
super.finalize();
@@ -5357,7 +5358,8 @@ public final class PowerManagerService extends SystemService
if (DEBUG_SPEW) {
Slog.d(TAG, "Acquiring suspend blocker \"" + mName + "\".");
}
- Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_POWER,
+ "SuspendBlockers", mName, mNameHash);
mNativeWrapper.nativeAcquireSuspendBlocker(mName);
}
}
@@ -5378,7 +5380,10 @@ public final class PowerManagerService extends SystemService
Slog.d(TAG, "Releasing suspend blocker \"" + mName + "\".");
}
mNativeWrapper.nativeReleaseSuspendBlocker(mName);
- Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0);
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) {
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER,
+ "SuspendBlockers", mNameHash);
+ }
} else if (mReferenceCount < 0) {
Slog.wtf(TAG, "Suspend blocker \"" + mName
+ "\" was released without being acquired!", new Throwable());
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index c6128f9f6cef..8a8486038558 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -6813,20 +6813,31 @@ public class BatteryStatsImpl extends BatteryStats {
synchronized (mModemNetworkLock) {
if (displayTransport == TRANSPORT_CELLULAR) {
mModemIfaces = includeInStringArray(mModemIfaces, iface);
- if (DEBUG) Slog.d(TAG, "Note mobile iface " + iface + ": " + mModemIfaces);
+ if (DEBUG) {
+ Slog.d(TAG, "Note mobile iface " + iface + ": "
+ + Arrays.toString(mModemIfaces));
+ }
} else {
mModemIfaces = excludeFromStringArray(mModemIfaces, iface);
- if (DEBUG) Slog.d(TAG, "Note non-mobile iface " + iface + ": " + mModemIfaces);
+ if (DEBUG) {
+ Slog.d(TAG, "Note non-mobile iface " + iface + ": "
+ + Arrays.toString(mModemIfaces));
+ }
}
}
synchronized (mWifiNetworkLock) {
if (displayTransport == TRANSPORT_WIFI) {
mWifiIfaces = includeInStringArray(mWifiIfaces, iface);
- if (DEBUG) Slog.d(TAG, "Note wifi iface " + iface + ": " + mWifiIfaces);
+ if (DEBUG) {
+ Slog.d(TAG, "Note wifi iface " + iface + ": " + Arrays.toString(mWifiIfaces));
+ }
} else {
mWifiIfaces = excludeFromStringArray(mWifiIfaces, iface);
- if (DEBUG) Slog.d(TAG, "Note non-wifi iface " + iface + ": " + mWifiIfaces);
+ if (DEBUG) {
+ Slog.d(TAG, "Note non-wifi iface " + iface + ": "
+ + Arrays.toString(mWifiIfaces));
+ }
}
}
}
@@ -12622,8 +12633,7 @@ public class BatteryStatsImpl extends BatteryStats {
private void updateCpuMeasuredEnergyStatsLocked(@NonNull long[] clusterChargeUC,
@NonNull CpuDeltaPowerAccumulator accumulator) {
if (DEBUG_ENERGY) {
- Slog.d(TAG,
- "Updating cpu cluster stats: " + clusterChargeUC.toString());
+ Slog.d(TAG, "Updating cpu cluster stats: " + Arrays.toString(clusterChargeUC));
}
if (mGlobalMeasuredEnergyStats == null) {
return;
diff --git a/services/core/java/com/android/server/timedetector/ConfigurationInternal.java b/services/core/java/com/android/server/timedetector/ConfigurationInternal.java
index 372bcc6b07ca..46f335ef9fa2 100644
--- a/services/core/java/com/android/server/timedetector/ConfigurationInternal.java
+++ b/services/core/java/com/android/server/timedetector/ConfigurationInternal.java
@@ -46,6 +46,7 @@ public final class ConfigurationInternal {
private final boolean mAutoDetectionSupported;
private final int mSystemClockUpdateThresholdMillis;
+ private final int mSystemClockConfidenceUpgradeThresholdMillis;
private final Instant mAutoSuggestionLowerBound;
private final Instant mManualSuggestionLowerBound;
private final Instant mSuggestionUpperBound;
@@ -57,6 +58,8 @@ public final class ConfigurationInternal {
private ConfigurationInternal(Builder builder) {
mAutoDetectionSupported = builder.mAutoDetectionSupported;
mSystemClockUpdateThresholdMillis = builder.mSystemClockUpdateThresholdMillis;
+ mSystemClockConfidenceUpgradeThresholdMillis =
+ builder.mSystemClockConfidenceUpgradeThresholdMillis;
mAutoSuggestionLowerBound = Objects.requireNonNull(builder.mAutoSuggestionLowerBound);
mManualSuggestionLowerBound = Objects.requireNonNull(builder.mManualSuggestionLowerBound);
mSuggestionUpperBound = Objects.requireNonNull(builder.mSuggestionUpperBound);
@@ -82,6 +85,17 @@ public final class ConfigurationInternal {
}
/**
+ * Return the absolute threshold at/below which the system clock confidence can be upgraded.
+ * i.e. if the detector receives a high-confidence time and the current system clock is +/- this
+ * value from that time and the confidence in the time is low, then the device's confidence in
+ * the current system clock time can be upgraded. This needs to be an amount users would
+ * consider "close enough".
+ */
+ public int getSystemClockConfidenceUpgradeThresholdMillis() {
+ return mSystemClockConfidenceUpgradeThresholdMillis;
+ }
+
+ /**
* Returns the lower bound for valid automatic time suggestions. It is guaranteed to be in the
* past, i.e. it is unrelated to the current system clock time.
* It holds no other meaning; it could be related to when the device system image was built,
@@ -242,6 +256,8 @@ public final class ConfigurationInternal {
return "ConfigurationInternal{"
+ "mAutoDetectionSupported=" + mAutoDetectionSupported
+ ", mSystemClockUpdateThresholdMillis=" + mSystemClockUpdateThresholdMillis
+ + ", mSystemClockConfidenceUpgradeThresholdMillis="
+ + mSystemClockConfidenceUpgradeThresholdMillis
+ ", mAutoSuggestionLowerBound=" + mAutoSuggestionLowerBound
+ "(" + mAutoSuggestionLowerBound.toEpochMilli() + ")"
+ ", mManualSuggestionLowerBound=" + mManualSuggestionLowerBound
@@ -258,6 +274,7 @@ public final class ConfigurationInternal {
static final class Builder {
private boolean mAutoDetectionSupported;
private int mSystemClockUpdateThresholdMillis;
+ private int mSystemClockConfidenceUpgradeThresholdMillis;
@NonNull private Instant mAutoSuggestionLowerBound;
@NonNull private Instant mManualSuggestionLowerBound;
@NonNull private Instant mSuggestionUpperBound;
@@ -286,68 +303,55 @@ public final class ConfigurationInternal {
this.mAutoDetectionEnabledSetting = toCopy.mAutoDetectionEnabledSetting;
}
- /**
- * Sets whether the user is allowed to configure time settings on this device.
- */
+ /** See {@link ConfigurationInternal#isUserConfigAllowed()}. */
Builder setUserConfigAllowed(boolean userConfigAllowed) {
mUserConfigAllowed = userConfigAllowed;
return this;
}
- /**
- * Sets whether automatic time detection is supported on this device.
- */
+ /** See {@link ConfigurationInternal#isAutoDetectionSupported()}. */
public Builder setAutoDetectionSupported(boolean supported) {
mAutoDetectionSupported = supported;
return this;
}
- /**
- * Sets the absolute threshold below which the system clock need not be updated. i.e. if
- * setting the system clock would adjust it by less than this (either backwards or forwards)
- * then it need not be set.
- */
+ /** See {@link ConfigurationInternal#getSystemClockUpdateThresholdMillis()}. */
public Builder setSystemClockUpdateThresholdMillis(int systemClockUpdateThresholdMillis) {
mSystemClockUpdateThresholdMillis = systemClockUpdateThresholdMillis;
return this;
}
- /**
- * Sets the lower bound for valid automatic time suggestions.
- */
+ /** See {@link ConfigurationInternal#getSystemClockConfidenceUpgradeThresholdMillis()}. */
+ public Builder setSystemClockConfidenceUpgradeThresholdMillis(int thresholdMillis) {
+ mSystemClockConfidenceUpgradeThresholdMillis = thresholdMillis;
+ return this;
+ }
+
+ /** See {@link ConfigurationInternal#getAutoSuggestionLowerBound()}. */
public Builder setAutoSuggestionLowerBound(@NonNull Instant autoSuggestionLowerBound) {
mAutoSuggestionLowerBound = Objects.requireNonNull(autoSuggestionLowerBound);
return this;
}
- /**
- * Sets the lower bound for valid manual time suggestions.
- */
+ /** See {@link ConfigurationInternal#getManualSuggestionLowerBound()}. */
public Builder setManualSuggestionLowerBound(@NonNull Instant manualSuggestionLowerBound) {
mManualSuggestionLowerBound = Objects.requireNonNull(manualSuggestionLowerBound);
return this;
}
- /**
- * Sets the upper bound for valid time suggestions (manual and automatic).
- */
+ /** See {@link ConfigurationInternal#getSuggestionUpperBound()}. */
public Builder setSuggestionUpperBound(@NonNull Instant suggestionUpperBound) {
mSuggestionUpperBound = Objects.requireNonNull(suggestionUpperBound);
return this;
}
- /**
- * Sets the order to look at time suggestions when automatically detecting time.
- * See {@code #ORIGIN_} constants
- */
+ /** See {@link ConfigurationInternal#getAutoOriginPriorities()}. */
public Builder setOriginPriorities(@NonNull @Origin int... originPriorities) {
mOriginPriorities = Objects.requireNonNull(originPriorities);
return this;
}
- /**
- * Sets the value of the automatic time detection enabled setting for this device.
- */
+ /** See {@link ConfigurationInternal#getAutoDetectionEnabledSetting()}. */
Builder setAutoDetectionEnabledSetting(boolean autoDetectionEnabledSetting) {
mAutoDetectionEnabledSetting = autoDetectionEnabledSetting;
return this;
diff --git a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
index 3e02b463284d..4972412472f1 100644
--- a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
+++ b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
@@ -16,16 +16,21 @@
package com.android.server.timedetector;
+import android.annotation.CurrentTimeMillisLong;
import android.annotation.NonNull;
-import android.app.AlarmManager;
import android.content.Context;
import android.os.Handler;
import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Slog;
+import com.android.server.AlarmManagerInternal;
+import com.android.server.LocalServices;
+import com.android.server.SystemClockTime;
+import com.android.server.SystemClockTime.TimeConfidence;
import com.android.server.timezonedetector.ConfigurationChangeListener;
+import java.io.PrintWriter;
import java.util.Objects;
/**
@@ -38,7 +43,7 @@ final class EnvironmentImpl implements TimeDetectorStrategyImpl.Environment {
@NonNull private final Handler mHandler;
@NonNull private final ServiceConfigAccessor mServiceConfigAccessor;
@NonNull private final PowerManager.WakeLock mWakeLock;
- @NonNull private final AlarmManager mAlarmManager;
+ @NonNull private final AlarmManagerInternal mAlarmManagerInternal;
EnvironmentImpl(@NonNull Context context, @NonNull Handler handler,
@NonNull ServiceConfigAccessor serviceConfigAccessor) {
@@ -49,7 +54,8 @@ final class EnvironmentImpl implements TimeDetectorStrategyImpl.Environment {
mWakeLock = Objects.requireNonNull(
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG));
- mAlarmManager = Objects.requireNonNull(context.getSystemService(AlarmManager.class));
+ mAlarmManagerInternal = Objects.requireNonNull(
+ LocalServices.getService(AlarmManagerInternal.class));
}
@Override
@@ -84,9 +90,22 @@ final class EnvironmentImpl implements TimeDetectorStrategyImpl.Environment {
}
@Override
- public void setSystemClock(long newTimeMillis) {
+ public @TimeConfidence int systemClockConfidence() {
+ return SystemClockTime.getTimeConfidence();
+ }
+
+ @Override
+ public void setSystemClock(
+ @CurrentTimeMillisLong long newTimeMillis, @TimeConfidence int confidence,
+ @NonNull String logMsg) {
+ checkWakeLockHeld();
+ mAlarmManagerInternal.setTime(newTimeMillis, confidence, logMsg);
+ }
+
+ @Override
+ public void setSystemClockConfidence(@TimeConfidence int confidence, @NonNull String logMsg) {
checkWakeLockHeld();
- mAlarmManager.setTime(newTimeMillis);
+ SystemClockTime.setConfidence(confidence, logMsg);
}
@Override
@@ -100,4 +119,13 @@ final class EnvironmentImpl implements TimeDetectorStrategyImpl.Environment {
Slog.wtf(LOG_TAG, "WakeLock " + mWakeLock + " not held");
}
}
-}
+
+ @Override
+ public void addDebugLogEntry(@NonNull String logMsg) {
+ SystemClockTime.addDebugLogEntry(logMsg);
+ }
+
+ @Override
+ public void dumpDebugLog(@NonNull PrintWriter printWriter) {
+ SystemClockTime.dump(printWriter);
+ }}
diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java
index 888304aa199b..0ea5f7a105d1 100644
--- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java
+++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java
@@ -68,6 +68,15 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor {
private static final int SYSTEM_CLOCK_UPDATE_THRESHOLD_MILLIS_DEFAULT = 2 * 1000;
/**
+ * An absolute threshold at/below which the system clock confidence can be upgraded. i.e. if the
+ * detector receives a high-confidence time and the current system clock is +/- this value from
+ * that time and the confidence in the time is low, then the device's confidence in the current
+ * system clock time can be upgraded. This needs to be an amount users would consider
+ * "close enough".
+ */
+ private static final int SYSTEM_CLOCK_CONFIRMATION_THRESHOLD_MILLIS = 1000;
+
+ /**
* By default telephony and network only suggestions are accepted and telephony takes
* precedence over network.
*/
@@ -236,6 +245,8 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor {
.setAutoDetectionSupported(isAutoDetectionSupported())
.setAutoDetectionEnabledSetting(getAutoDetectionEnabledSetting())
.setSystemClockUpdateThresholdMillis(getSystemClockUpdateThresholdMillis())
+ .setSystemClockConfidenceUpgradeThresholdMillis(
+ getSystemClockConfidenceUpgradeThresholdMillis())
.setAutoSuggestionLowerBound(getAutoSuggestionLowerBound())
.setManualSuggestionLowerBound(timeDetectorHelper.getManualSuggestionLowerBound())
.setSuggestionUpperBound(timeDetectorHelper.getSuggestionUpperBound())
@@ -285,6 +296,10 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor {
return mSystemClockUpdateThresholdMillis;
}
+ private int getSystemClockConfidenceUpgradeThresholdMillis() {
+ return SYSTEM_CLOCK_CONFIRMATION_THRESHOLD_MILLIS;
+ }
+
@NonNull
private Instant getAutoSuggestionLowerBound() {
return mServerFlags.getOptionalInstant(KEY_TIME_DETECTOR_LOWER_BOUND_MILLIS_OVERRIDE)
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
index 547cf9d32aa1..fe2760e9ce10 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
@@ -16,6 +16,7 @@
package com.android.server.timedetector;
+import static com.android.server.SystemClockTime.TIME_CONFIDENCE_HIGH;
import static com.android.server.timedetector.TimeDetectorStrategy.originToString;
import android.annotation.CurrentTimeMillisLong;
@@ -23,7 +24,6 @@ import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
-import android.app.AlarmManager;
import android.app.time.ExternalTimeSuggestion;
import android.app.timedetector.ManualTimeSuggestion;
import android.app.timedetector.TelephonyTimeSuggestion;
@@ -31,24 +31,24 @@ import android.content.Context;
import android.os.Handler;
import android.os.TimestampedValue;
import android.util.IndentingPrintWriter;
-import android.util.LocalLog;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemClockTime;
+import com.android.server.SystemClockTime.TimeConfidence;
import com.android.server.timezonedetector.ArrayMapWithHistory;
import com.android.server.timezonedetector.ConfigurationChangeListener;
import com.android.server.timezonedetector.ReferenceWithHistory;
+import java.io.PrintWriter;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Objects;
/**
- * An implementation of {@link TimeDetectorStrategy} that passes telephony and manual suggestions to
- * {@link AlarmManager}. When there are multiple telephony sources, the one with the lowest ID is
- * used unless the data becomes too stale.
+ * The real implementation of {@link TimeDetectorStrategy}.
*
* <p>Most public methods are marked synchronized to ensure thread safety around internal state.
*/
@@ -84,13 +84,6 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
*/
private static final int KEEP_SUGGESTION_HISTORY_SIZE = 10;
- /**
- * A log that records the decisions / decision metadata that affected the device's system clock
- * time. This is logged in bug reports to assist with debugging issues with detection.
- */
- @NonNull
- private final LocalLog mTimeChangesLog = new LocalLog(30, false /* useLocalTimestamps */);
-
@NonNull
private final Environment mEnvironment;
@@ -157,11 +150,30 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
@CurrentTimeMillisLong
long systemClockMillis();
- /** Sets the device system clock. The WakeLock must be held. */
- void setSystemClock(@CurrentTimeMillisLong long newTimeMillis);
+ /** Returns the system clock confidence value. */
+ @TimeConfidence int systemClockConfidence();
+
+ /** Sets the device system clock and confidence. The WakeLock must be held. */
+ void setSystemClock(
+ @CurrentTimeMillisLong long newTimeMillis, @TimeConfidence int confidence,
+ @NonNull String logMsg);
+
+ /** Sets the device system clock confidence. The WakeLock must be held. */
+ void setSystemClockConfidence(@TimeConfidence int confidence, @NonNull String logMsg);
/** Release the wake lock acquired by a call to {@link #acquireWakeLock()}. */
void releaseWakeLock();
+
+
+ /**
+ * Adds a standalone entry to the time debug log.
+ */
+ void addDebugLogEntry(@NonNull String logMsg);
+
+ /**
+ * Dumps the time debug log to the supplied {@link PrintWriter}.
+ */
+ void dumpDebugLog(PrintWriter printWriter);
}
static TimeDetectorStrategy create(
@@ -250,7 +262,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
}
String cause = "Manual time suggestion received: suggestion=" + suggestion;
- return setSystemClockIfRequired(ORIGIN_MANUAL, newUnixEpochTime, cause);
+ return setSystemClockAndConfidenceIfRequired(ORIGIN_MANUAL, newUnixEpochTime, cause);
}
@Override
@@ -318,7 +330,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
String logMsg = "handleConfigurationInternalChanged:"
+ " oldConfiguration=" + mCurrentConfigurationInternal
+ ", newConfiguration=" + currentUserConfig;
- logTimeDetectorChange(logMsg);
+ addDebugLogEntry(logMsg);
mCurrentConfigurationInternal = currentUserConfig;
boolean autoDetectionEnabled =
@@ -335,11 +347,11 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
}
}
- private void logTimeDetectorChange(@NonNull String logMsg) {
+ private void addDebugLogEntry(@NonNull String logMsg) {
if (DBG) {
Slog.d(LOG_TAG, logMsg);
}
- mTimeChangesLog.log(logMsg);
+ mEnvironment.addDebugLogEntry(logMsg);
}
@Override
@@ -356,10 +368,11 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
long systemClockMillis = mEnvironment.systemClockMillis();
ipw.printf("mEnvironment.systemClockMillis()=%s (%s)\n",
Instant.ofEpochMilli(systemClockMillis), systemClockMillis);
+ ipw.println("mEnvironment.systemClockConfidence()=" + mEnvironment.systemClockConfidence());
ipw.println("Time change log:");
ipw.increaseIndent(); // level 2
- mTimeChangesLog.dump(ipw);
+ SystemClockTime.dump(ipw);
ipw.decreaseIndent(); // level 2
ipw.println("Telephony suggestion history:");
@@ -494,11 +507,6 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
@GuardedBy("this")
private void doAutoTimeDetection(@NonNull String detectionReason) {
- if (!mCurrentConfigurationInternal.getAutoDetectionEnabledBehavior()) {
- // Avoid doing unnecessary work with this (race-prone) check.
- return;
- }
-
// Try the different origins one at a time.
int[] originPriorities = mCurrentConfigurationInternal.getAutoOriginPriorities();
for (int origin : originPriorities) {
@@ -544,7 +552,14 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
// Update the system clock if a good suggestion has been found.
if (newUnixEpochTime != null) {
- setSystemClockIfRequired(origin, newUnixEpochTime, cause);
+ if (mCurrentConfigurationInternal.getAutoDetectionEnabledBehavior()) {
+ setSystemClockAndConfidenceIfRequired(origin, newUnixEpochTime, cause);
+ } else {
+ // An automatically detected time can be used to raise the confidence in the
+ // current time even if the device is set to only allow user input for the time
+ // itself.
+ upgradeSystemClockConfidenceIfRequired(newUnixEpochTime, cause);
+ }
return;
}
}
@@ -718,14 +733,18 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
}
@GuardedBy("this")
- private boolean setSystemClockIfRequired(
+ private boolean setSystemClockAndConfidenceIfRequired(
@Origin int origin, @NonNull TimestampedValue<Long> time, @NonNull String cause) {
+ // Any time set through this class is inherently high confidence. Either it came directly
+ // from a user, or it was detected automatically.
+ @TimeConfidence final int newTimeConfidence = TIME_CONFIDENCE_HIGH;
boolean isOriginAutomatic = isOriginAutomatic(origin);
if (isOriginAutomatic) {
if (!mCurrentConfigurationInternal.getAutoDetectionEnabledBehavior()) {
if (DBG) {
- Slog.d(LOG_TAG, "Auto time detection is not enabled."
+ Slog.d(LOG_TAG,
+ "Auto time detection is not enabled / no confidence update is needed."
+ " origin=" + originToString(origin)
+ ", time=" + time
+ ", cause=" + cause);
@@ -746,7 +765,57 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
mEnvironment.acquireWakeLock();
try {
- return setSystemClockUnderWakeLock(origin, time, cause);
+ return setSystemClockAndConfidenceUnderWakeLock(origin, time, newTimeConfidence, cause);
+ } finally {
+ mEnvironment.releaseWakeLock();
+ }
+ }
+
+ /**
+ * Upgrades the system clock confidence if the current time matches the supplied auto-detected
+ * time. The method never changes the system clock and it never lowers the confidence. It only
+ * raises the confidence if the supplied time is within the configured threshold of the current
+ * system clock time.
+ */
+ @GuardedBy("this")
+ private void upgradeSystemClockConfidenceIfRequired(
+ @NonNull TimestampedValue<Long> autoDetectedUnixEpochTime, @NonNull String cause) {
+ @TimeConfidence int newTimeConfidence = TIME_CONFIDENCE_HIGH;
+ @TimeConfidence int currentTimeConfidence = mEnvironment.systemClockConfidence();
+ boolean timeNeedsConfirmation = currentTimeConfidence < newTimeConfidence;
+ if (!timeNeedsConfirmation) {
+ return;
+ }
+
+ // All system clock calculation take place under a wake lock.
+ mEnvironment.acquireWakeLock();
+ try {
+ // Check if the specified time matches the current system clock time (closely
+ // enough) to raise the confidence.
+ long elapsedRealtimeMillis = mEnvironment.elapsedRealtimeMillis();
+ long actualSystemClockMillis = mEnvironment.systemClockMillis();
+ long adjustedAutoDetectedUnixEpochMillis = TimeDetectorStrategy.getTimeAt(
+ autoDetectedUnixEpochTime, elapsedRealtimeMillis);
+ long absTimeDifferenceMillis =
+ Math.abs(adjustedAutoDetectedUnixEpochMillis - actualSystemClockMillis);
+ int confidenceUpgradeThresholdMillis =
+ mCurrentConfigurationInternal.getSystemClockConfidenceUpgradeThresholdMillis();
+ boolean updateConfidenceRequired =
+ absTimeDifferenceMillis <= confidenceUpgradeThresholdMillis;
+ if (updateConfidenceRequired) {
+ String logMsg = "Upgrade system clock confidence."
+ + " autoDetectedUnixEpochTime=" + autoDetectedUnixEpochTime
+ + " newTimeConfidence=" + newTimeConfidence
+ + " cause=" + cause
+ + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+ + " (old) actualSystemClockMillis=" + actualSystemClockMillis
+ + " currentTimeConfidence=" + currentTimeConfidence;
+ if (DBG) {
+ Slog.d(LOG_TAG, logMsg);
+ }
+
+ mEnvironment.setSystemClockConfidence(newTimeConfidence, logMsg);
+ }
} finally {
mEnvironment.releaseWakeLock();
}
@@ -757,8 +826,9 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
}
@GuardedBy("this")
- private boolean setSystemClockUnderWakeLock(
- @Origin int origin, @NonNull TimestampedValue<Long> newTime, @NonNull String cause) {
+ private boolean setSystemClockAndConfidenceUnderWakeLock(
+ @Origin int origin, @NonNull TimestampedValue<Long> newTime,
+ @TimeConfidence int newTimeConfidence, @NonNull String cause) {
long elapsedRealtimeMillis = mEnvironment.elapsedRealtimeMillis();
boolean isOriginAutomatic = isOriginAutomatic(origin);
@@ -776,6 +846,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
"System clock has not tracked elapsed real time clock. A clock may"
+ " be inaccurate or something unexpectedly set the system"
+ " clock."
+ + " origin=" + originToString(origin)
+ " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+ " expectedTimeMillis=" + expectedTimeMillis
+ " actualTimeMillis=" + actualSystemClockMillis
@@ -784,44 +855,72 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
}
}
+ // If the new signal would make sufficient difference to the system clock or mean a change
+ // in confidence then system state must be updated.
+
// Adjust for the time that has elapsed since the signal was received.
long newSystemClockMillis = TimeDetectorStrategy.getTimeAt(newTime, elapsedRealtimeMillis);
-
- // Check if the new signal would make sufficient difference to the system clock. If it's
- // below the threshold then ignore it.
long absTimeDifference = Math.abs(newSystemClockMillis - actualSystemClockMillis);
long systemClockUpdateThreshold =
mCurrentConfigurationInternal.getSystemClockUpdateThresholdMillis();
- if (absTimeDifference < systemClockUpdateThreshold) {
+ boolean updateSystemClockRequired = absTimeDifference >= systemClockUpdateThreshold;
+
+ @TimeConfidence int currentTimeConfidence = mEnvironment.systemClockConfidence();
+ boolean updateConfidenceRequired = newTimeConfidence > currentTimeConfidence;
+
+ if (updateSystemClockRequired) {
+ String logMsg = "Set system clock & confidence."
+ + " origin=" + originToString(origin)
+ + " newTime=" + newTime
+ + " newTimeConfidence=" + newTimeConfidence
+ + " cause=" + cause
+ + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+ + " (old) actualSystemClockMillis=" + actualSystemClockMillis
+ + " newSystemClockMillis=" + newSystemClockMillis
+ + " currentTimeConfidence=" + currentTimeConfidence;
+ mEnvironment.setSystemClock(newSystemClockMillis, newTimeConfidence, logMsg);
if (DBG) {
- Slog.d(LOG_TAG, "Not setting system clock. New time and"
- + " system clock are close enough."
- + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+ Slog.d(LOG_TAG, logMsg);
+ }
+
+ // CLOCK_PARANOIA : Record the last time this class set the system clock due to an
+ // auto-time signal, or clear the record it is being done manually.
+ if (isOriginAutomatic(origin)) {
+ mLastAutoSystemClockTimeSet = newTime;
+ } else {
+ mLastAutoSystemClockTimeSet = null;
+ }
+ } else if (updateConfidenceRequired) {
+ // Only the confidence needs updating. This path is separate from a system clock update
+ // to deliberately avoid touching the system clock's value when it's not needed. Doing
+ // so could introduce inaccuracies or cause unnecessary wear in RTC hardware or
+ // associated storage.
+ String logMsg = "Set system clock confidence."
+ + " origin=" + originToString(origin)
+ + " newTime=" + newTime
+ + " newTimeConfidence=" + newTimeConfidence
+ + " cause=" + cause
+ + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+ + " (old) actualSystemClockMillis=" + actualSystemClockMillis
+ + " newSystemClockMillis=" + newSystemClockMillis
+ + " currentTimeConfidence=" + currentTimeConfidence;
+ if (DBG) {
+ Slog.d(LOG_TAG, logMsg);
+ }
+ mEnvironment.setSystemClockConfidence(newTimeConfidence, logMsg);
+ } else {
+ // Neither clock nor confidence need updating.
+ if (DBG) {
+ Slog.d(LOG_TAG, "Not setting system clock or confidence."
+ + " origin=" + originToString(origin)
+ " newTime=" + newTime
+ + " newTimeConfidence=" + newTimeConfidence
+ " cause=" + cause
+ + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
+ " systemClockUpdateThreshold=" + systemClockUpdateThreshold
- + " absTimeDifference=" + absTimeDifference);
+ + " absTimeDifference=" + absTimeDifference
+ + " currentTimeConfidence=" + currentTimeConfidence);
}
- return true;
- }
-
- mEnvironment.setSystemClock(newSystemClockMillis);
- String logMsg = "Set system clock using time=" + newTime
- + " cause=" + cause
- + " elapsedRealtimeMillis=" + elapsedRealtimeMillis
- + " (old) actualSystemClockMillis=" + actualSystemClockMillis
- + " newSystemClockMillis=" + newSystemClockMillis;
- if (DBG) {
- Slog.d(LOG_TAG, logMsg);
- }
- mTimeChangesLog.log(logMsg);
-
- // CLOCK_PARANOIA : Record the last time this class set the system clock due to an auto-time
- // signal, or clear the record it is being done manually.
- if (isOriginAutomatic(origin)) {
- mLastAutoSystemClockTimeSet = newTime;
- } else {
- mLastAutoSystemClockTimeSet = null;
}
return true;
}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 5d6ffd8b8ead..7e93d623f3e0 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -53,6 +53,7 @@ cc_library_static {
"com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker.cpp",
"com_android_server_stats_pull_StatsPullAtomService.cpp",
"com_android_server_storage_AppFuseBridge.cpp",
+ "com_android_server_SystemClockTime.cpp",
"com_android_server_SystemServer.cpp",
"com_android_server_tv_TvUinputBridge.cpp",
"com_android_server_tv_TvInputHal.cpp",
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index 9abf107c780a..2584b86f53db 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -12,6 +12,7 @@ per-file com_android_server_power_PowerManagerService.* = michaelwr@google.com,
per-file com_android_server_am_BatteryStatsService.cpp = file:/BATTERY_STATS_OWNERS
per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
+per-file com_android_server_SystemClock* = file:/services/core/java/com/android/server/timedetector/OWNERS
per-file com_android_server_Usb* = file:/services/usb/OWNERS
per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS
per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS
diff --git a/services/core/jni/com_android_server_SystemClockTime.cpp b/services/core/jni/com_android_server_SystemClockTime.cpp
new file mode 100644
index 000000000000..9db4c429f0c7
--- /dev/null
+++ b/services/core/jni/com_android_server_SystemClockTime.cpp
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "SystemClockTime"
+
+#include <android-base/file.h>
+#include <android-base/unique_fd.h>
+#include <linux/rtc.h>
+#include <nativehelper/JNIHelp.h>
+#include <utils/Log.h>
+#include <utils/String8.h>
+
+#include "jni.h"
+
+namespace android {
+
+class SystemClockImpl {
+public:
+ SystemClockImpl(const std::string &rtc_dev) : rtc_dev{rtc_dev} {}
+
+ int setTime(struct timeval *tv);
+
+private:
+ std::string rtc_dev;
+};
+
+int SystemClockImpl::setTime(struct timeval *tv) {
+ if (settimeofday(tv, NULL) == -1) {
+ ALOGV("settimeofday() failed: %s", strerror(errno));
+ return -1;
+ }
+
+ android::base::unique_fd fd{open(rtc_dev.c_str(), O_RDWR)};
+ if (!fd.ok()) {
+ ALOGE("Unable to open %s: %s", rtc_dev.c_str(), strerror(errno));
+ return -1;
+ }
+
+ struct tm tm;
+ if (!gmtime_r(&tv->tv_sec, &tm)) {
+ ALOGV("gmtime_r() failed: %s", strerror(errno));
+ return -1;
+ }
+
+ struct rtc_time rtc = {};
+ rtc.tm_sec = tm.tm_sec;
+ rtc.tm_min = tm.tm_min;
+ rtc.tm_hour = tm.tm_hour;
+ rtc.tm_mday = tm.tm_mday;
+ rtc.tm_mon = tm.tm_mon;
+ rtc.tm_year = tm.tm_year;
+ rtc.tm_wday = tm.tm_wday;
+ rtc.tm_yday = tm.tm_yday;
+ rtc.tm_isdst = tm.tm_isdst;
+ if (ioctl(fd, RTC_SET_TIME, &rtc) == -1) {
+ ALOGV("RTC_SET_TIME ioctl failed: %s", strerror(errno));
+ return -1;
+ }
+
+ return 0;
+}
+
+static jlong com_android_server_SystemClockTime_init(JNIEnv *, jobject) {
+ // Find the wall clock RTC. We expect this always to be /dev/rtc0, but
+ // check the /dev/rtc symlink first so that legacy devices that don't use
+ // rtc0 can add a symlink rather than need to carry a local patch to this
+ // code.
+ //
+ // TODO: if you're reading this in a world where all devices are using the
+ // GKI, you can remove the readlink and just assume /dev/rtc0.
+ std::string dev_rtc;
+ if (!android::base::Readlink("/dev/rtc", &dev_rtc)) {
+ dev_rtc = "/dev/rtc0";
+ }
+
+ std::unique_ptr<SystemClockImpl> system_clock{new SystemClockImpl(dev_rtc)};
+ return reinterpret_cast<jlong>(system_clock.release());
+}
+
+static jint com_android_server_SystemClockTime_setTime(JNIEnv *, jobject, jlong nativeData,
+ jlong millis) {
+ SystemClockImpl *impl = reinterpret_cast<SystemClockImpl *>(nativeData);
+
+ if (millis <= 0 || millis / 1000LL >= std::numeric_limits<time_t>::max()) {
+ return -1;
+ }
+
+ struct timeval tv;
+ tv.tv_sec = (millis / 1000LL);
+ tv.tv_usec = ((millis % 1000LL) * 1000LL);
+
+ ALOGD("Setting time of day to sec=%ld", tv.tv_sec);
+
+ int ret = impl->setTime(&tv);
+ if (ret < 0) {
+ ALOGW("Unable to set rtc to %ld: %s", tv.tv_sec, strerror(errno));
+ ret = -1;
+ }
+ return ret;
+}
+
+static const JNINativeMethod sMethods[] = {
+ /* name, signature, funcPtr */
+ {"init", "()J", (void *)com_android_server_SystemClockTime_init},
+ {"setTime", "(JJ)I", (void *)com_android_server_SystemClockTime_setTime},
+};
+
+int register_com_android_server_SystemClockTime(JNIEnv *env) {
+ return jniRegisterNativeMethods(env, "com/android/server/SystemClockTime", sMethods,
+ NELEM(sMethods));
+}
+
+} /* namespace android */
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 00bef0935308..00f851f9f4ff 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -65,6 +65,7 @@ int register_android_server_companion_virtual_InputController(JNIEnv* env);
int register_android_server_app_GameManagerService(JNIEnv* env);
int register_com_android_server_wm_TaskFpsCallbackController(JNIEnv* env);
int register_com_android_server_display_DisplayControl(JNIEnv* env);
+int register_com_android_server_SystemClockTime(JNIEnv* env);
};
using namespace android;
@@ -122,5 +123,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
register_android_server_app_GameManagerService(env);
register_com_android_server_wm_TaskFpsCallbackController(env);
register_com_android_server_display_DisplayControl(env);
+ register_com_android_server_SystemClockTime(env);
return JNI_VERSION_1_4;
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index d8f282aee5a8..9e449aedf0d9 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -331,6 +331,8 @@ public final class SystemServer implements Dumpable {
"com.android.server.wallpaper.WallpaperManagerService$Lifecycle";
private static final String AUTO_FILL_MANAGER_SERVICE_CLASS =
"com.android.server.autofill.AutofillManagerService";
+ private static final String CREDENTIAL_MANAGER_SERVICE_CLASS =
+ "com.android.server.credentials.CredentialManagerService";
private static final String CONTENT_CAPTURE_MANAGER_SERVICE_CLASS =
"com.android.server.contentcapture.ContentCaptureManagerService";
private static final String TRANSLATION_MANAGER_SERVICE_CLASS =
@@ -2571,6 +2573,12 @@ public final class SystemServer implements Dumpable {
t.traceEnd();
}
+ if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_CREDENTIALS)) {
+ t.traceBegin("StartCredentialManagerService");
+ mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS);
+ t.traceEnd();
+ }
+
// Translation manager service
if (deviceHasConfigString(context, R.string.config_defaultTranslationService)) {
t.traceBegin("StartTranslationManagerService");
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 1b9282d426d0..830e2ac029c2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -168,6 +168,7 @@ import com.android.server.AppStateTracker;
import com.android.server.AppStateTrackerImpl;
import com.android.server.DeviceIdleInternal;
import com.android.server.LocalServices;
+import com.android.server.SystemClockTime.TimeConfidence;
import com.android.server.SystemService;
import com.android.server.pm.permission.PermissionManagerService;
import com.android.server.pm.permission.PermissionManagerServiceInternal;
@@ -346,10 +347,6 @@ public class AlarmManagerServiceTest {
}
@Override
- void setKernelTime(long millis) {
- }
-
- @Override
int getSystemUiUid(PackageManagerInternal unused) {
return SYSTEM_UI_UID;
}
@@ -361,7 +358,18 @@ public class AlarmManagerServiceTest {
}
@Override
- long getElapsedRealtime() {
+ void initializeTimeIfRequired() {
+ // No-op
+ }
+
+ @Override
+ void setCurrentTimeMillis(long unixEpochMillis,
+ @TimeConfidence int confidence, String logMsg) {
+ mNowRtcTest = unixEpochMillis;
+ }
+
+ @Override
+ long getElapsedRealtimeMillis() {
return mNowElapsedTest;
}
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 96c3823f8349..2d2c76c40b10 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -94,6 +94,7 @@ import android.view.Display;
import androidx.test.filters.SmallTest;
+import com.android.internal.widget.LockPatternUtils;
import com.android.server.FgThread;
import com.android.server.SystemService;
import com.android.server.am.UserState.KeyEvictedCallback;
@@ -834,8 +835,7 @@ public class UserControllerTest {
private void setUpAndStartUserInBackground(int userId) throws Exception {
setUpUser(userId, 0);
mUserController.startUser(userId, /* foreground= */ false);
- verify(mInjector.mStorageManagerMock, times(1))
- .unlockUserKey(userId, /* serialNumber= */ 0, /* secret= */ null);
+ verify(mInjector.mLockPatternUtilsMock, times(1)).unlockUserKeyIfUnsecured(userId);
mUserStates.put(userId, mUserController.getStartedUserState(userId));
}
@@ -843,8 +843,7 @@ public class UserControllerTest {
setUpUser(userId, UserInfo.FLAG_PROFILE, false, UserManager.USER_TYPE_PROFILE_MANAGED);
assertThat(mUserController.startProfile(userId)).isTrue();
- verify(mInjector.mStorageManagerMock, times(1))
- .unlockUserKey(userId, /* serialNumber= */ 0, /* secret= */ null);
+ verify(mInjector.mLockPatternUtilsMock, times(1)).unlockUserKeyIfUnsecured(userId);
mUserStates.put(userId, mUserController.getStartedUserState(userId));
}
@@ -966,6 +965,7 @@ public class UserControllerTest {
private final UserManagerInternal mUserManagerInternalMock;
private final WindowManagerService mWindowManagerMock;
private final KeyguardManager mKeyguardManagerMock;
+ private final LockPatternUtils mLockPatternUtilsMock;
private final Context mCtx;
@@ -982,6 +982,7 @@ public class UserControllerTest {
mStorageManagerMock = mock(IStorageManager.class);
mKeyguardManagerMock = mock(KeyguardManager.class);
when(mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(true);
+ mLockPatternUtilsMock = mock(LockPatternUtils.class);
}
@Override
@@ -1081,6 +1082,11 @@ public class UserControllerTest {
protected void dismissKeyguard(Runnable runnable, String reason) {
runnable.run();
}
+
+ @Override
+ protected LockPatternUtils getLockPatternUtils() {
+ return mLockPatternUtilsMock;
+ }
}
private static class TestHandler extends Handler {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java
new file mode 100644
index 000000000000..47b4bf547ff8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.biometrics.sensors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.biometrics.BiometricManager;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class AuthResultCoordinatorTest {
+ private AuthResultCoordinator mAuthResultCoordinator;
+
+ @Before
+ public void setUp() throws Exception {
+ mAuthResultCoordinator = new AuthResultCoordinator();
+ }
+
+ @Test
+ public void testDefaultMessage() {
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.FAILED,
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ }
+
+ @Test
+ public void testSingleMessageCoordinator() {
+ mAuthResultCoordinator.authenticatedFor(
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.AUTHENTICATED,
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ }
+
+ @Test
+ public void testLockout() {
+ mAuthResultCoordinator.lockedOutFor(
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.LOCKED_OUT,
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ }
+
+ @Test
+ public void testHigherStrengthPrecedence() {
+ mAuthResultCoordinator.authenticatedFor(
+ BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+ mAuthResultCoordinator.authenticatedFor(
+ BiometricManager.Authenticators.BIOMETRIC_WEAK);
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.AUTHENTICATED,
+ BiometricManager.Authenticators.BIOMETRIC_WEAK);
+
+ mAuthResultCoordinator.authenticatedFor(
+ BiometricManager.Authenticators.BIOMETRIC_STRONG);
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.AUTHENTICATED,
+ BiometricManager.Authenticators.BIOMETRIC_STRONG);
+ }
+
+ @Test
+ public void testAuthPrecedence() {
+ mAuthResultCoordinator.authenticatedFor(
+ BiometricManager.Authenticators.BIOMETRIC_WEAK);
+ mAuthResultCoordinator.lockedOutFor(
+ BiometricManager.Authenticators.BIOMETRIC_WEAK);
+ checkResult(mAuthResultCoordinator.getResult(),
+ AuthResult.AUTHENTICATED,
+ BiometricManager.Authenticators.BIOMETRIC_WEAK);
+
+ }
+
+ void checkResult(AuthResult res, int status,
+ @BiometricManager.Authenticators.Types int strength) {
+ assertThat(res.getStatus()).isEqualTo(status);
+ assertThat(res.getBiometricStrength()).isEqualTo(strength);
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java
new file mode 100644
index 000000000000..9bb0f58db520
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.biometrics.sensors;
+
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@Presubmit
+@SmallTest
+public class AuthSessionCoordinatorTest {
+ private static final int PRIMARY_USER = 0;
+ private static final int SECONDARY_USER = 10;
+
+ private AuthSessionCoordinator mCoordinator;
+
+ @Before
+ public void setUp() throws Exception {
+ mCoordinator = new AuthSessionCoordinator();
+ }
+
+ @Test
+ public void testUserUnlocked() {
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+
+ mCoordinator.authStartedFor(PRIMARY_USER, 1);
+ mCoordinator.authenticatedFor(PRIMARY_USER, BIOMETRIC_WEAK, 1);
+
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+ }
+
+ @Test
+ public void testUserCanAuthDuringLockoutOfSameSession() {
+ mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_STRONG);
+
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+
+ mCoordinator.authStartedFor(PRIMARY_USER, 1);
+ mCoordinator.authStartedFor(PRIMARY_USER, 2);
+ mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_WEAK, 2);
+
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ }
+
+ @Test
+ public void testMultiUserAuth() {
+ mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_STRONG);
+
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_STRONG)).isFalse();
+
+ mCoordinator.authStartedFor(PRIMARY_USER, 1);
+ mCoordinator.authStartedFor(PRIMARY_USER, 2);
+ mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_WEAK, 2);
+
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_STRONG)).isFalse();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java
new file mode 100644
index 000000000000..8baa1ce3f6c6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.biometrics.sensors;
+
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class MultiBiometricLockoutStateTest {
+ private static final int PRIMARY_USER = 0;
+ private MultiBiometricLockoutState mCoordinator;
+
+ private void unlockAllBiometrics() {
+ unlockAllBiometrics(mCoordinator, PRIMARY_USER);
+ }
+
+ private void lockoutAllBiometrics() {
+ lockoutAllBiometrics(mCoordinator, PRIMARY_USER);
+ }
+
+ private static void unlockAllBiometrics(MultiBiometricLockoutState coordinator, int userId) {
+ coordinator.onUserUnlocked(userId, BIOMETRIC_STRONG);
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_STRONG)).isTrue();
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_WEAK)).isTrue();
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_CONVENIENCE)).isTrue();
+ }
+
+ private static void lockoutAllBiometrics(MultiBiometricLockoutState coordinator, int userId) {
+ coordinator.onUserLocked(userId, BIOMETRIC_STRONG);
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_STRONG)).isFalse();
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_WEAK)).isFalse();
+ assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mCoordinator = new MultiBiometricLockoutState();
+ }
+
+ @Test
+ public void testInitialStateLockedOut() {
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+
+ @Test
+ public void testConvenienceLockout() {
+ unlockAllBiometrics();
+ mCoordinator.onUserLocked(PRIMARY_USER, BIOMETRIC_CONVENIENCE);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+
+ @Test
+ public void testWeakLockout() {
+ unlockAllBiometrics();
+ mCoordinator.onUserLocked(PRIMARY_USER, BIOMETRIC_WEAK);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+
+ @Test
+ public void testStrongLockout() {
+ unlockAllBiometrics();
+ mCoordinator.onUserLocked(PRIMARY_USER, BIOMETRIC_STRONG);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+
+ @Test
+ public void testConvenienceUnlock() {
+ lockoutAllBiometrics();
+ mCoordinator.onUserUnlocked(PRIMARY_USER, BIOMETRIC_CONVENIENCE);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ }
+
+ @Test
+ public void testWeakUnlock() {
+ lockoutAllBiometrics();
+ mCoordinator.onUserUnlocked(PRIMARY_USER, BIOMETRIC_WEAK);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ }
+
+ @Test
+ public void testStrongUnlock() {
+ lockoutAllBiometrics();
+ mCoordinator.onUserUnlocked(PRIMARY_USER, BIOMETRIC_STRONG);
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+ assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+ }
+
+ @Test
+ public void multiUser_userOneDoesNotAffectUserTwo() {
+ final int userOne = 1;
+ final int userTwo = 2;
+ MultiBiometricLockoutState coordinator = new MultiBiometricLockoutState();
+ lockoutAllBiometrics(coordinator, userOne);
+ lockoutAllBiometrics(coordinator, userTwo);
+
+ coordinator.onUserUnlocked(userOne, BIOMETRIC_WEAK);
+ assertThat(coordinator.canUserAuthenticate(userOne, BIOMETRIC_STRONG)).isFalse();
+ assertThat(coordinator.canUserAuthenticate(userOne, BIOMETRIC_WEAK)).isTrue();
+ assertThat(coordinator.canUserAuthenticate(userOne, BIOMETRIC_CONVENIENCE)).isTrue();
+
+ assertThat(coordinator.canUserAuthenticate(userTwo, BIOMETRIC_STRONG)).isFalse();
+ assertThat(coordinator.canUserAuthenticate(userTwo, BIOMETRIC_WEAK)).isFalse();
+ assertThat(coordinator.canUserAuthenticate(userTwo, BIOMETRIC_CONVENIENCE)).isFalse();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 57ded9905273..6388c7d52f10 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -236,10 +236,9 @@ public class VirtualDeviceManagerServiceTest {
.setBlockedActivities(getBlockedActivities())
.build();
mDeviceImpl = new VirtualDeviceImpl(mContext,
- mAssociationInfo, new Binder(), /* uid */ 0, mInputController,
- (int associationId) -> {
- }, mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback,
- params);
+ mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1,
+ mInputController, (int associationId) -> {}, mPendingTrampolineCallback,
+ mActivityListener, mRunningAppsChangedCallback, params);
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index e220841a3816..c934e653564e 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -168,16 +168,15 @@ public abstract class BaseLockSettingsServiceTests {
allUsers.add(SECONDARY_USER_INFO);
when(mUserManager.getUsers()).thenReturn(allUsers);
- when(mActivityManager.unlockUser(anyInt(), any(), any(), any())).thenAnswer(
- new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ when(mActivityManager.unlockUser2(anyInt(), any())).thenAnswer(
+ invocation -> {
Object[] args = invocation.getArguments();
- mStorageManager.unlockUser((int)args[0], (byte[])args[2],
- (IProgressListener) args[3]);
+ int userId = (int) args[0];
+ IProgressListener listener = (IProgressListener) args[1];
+ listener.onStarted(userId, null);
+ listener.onFinished(userId, null);
return true;
- }
- });
+ });
// Adding a fake Device Owner app which will enable escrow token support in LSS.
when(mDevicePolicyManager.getDeviceOwnerComponentOnAnyUser()).thenReturn(
@@ -215,37 +214,20 @@ public abstract class BaseLockSettingsServiceTests {
private IStorageManager setUpStorageManagerMock() throws RemoteException {
final IStorageManager sm = mock(IStorageManager.class);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) throws Throwable {
- Object[] args = invocation.getArguments();
- mStorageManager.addUserKeyAuth((int) args[0] /* userId */,
- (int) args[1] /* serialNumber */,
- (byte[]) args[2] /* secret */);
- return null;
- }
- }).when(sm).addUserKeyAuth(anyInt(), anyInt(), any());
+ doAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ mStorageManager.unlockUserKey(/* userId= */ (int) args[0],
+ /* secret= */ (byte[]) args[2]);
+ return null;
+ }).when(sm).unlockUserKey(anyInt(), anyInt(), any());
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) throws Throwable {
- Object[] args = invocation.getArguments();
- mStorageManager.clearUserKeyAuth((int) args[0] /* userId */,
- (int) args[1] /* serialNumber */,
- (byte[]) args[2] /* secret */);
- return null;
- }
- }).when(sm).clearUserKeyAuth(anyInt(), anyInt(), any());
+ doAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ mStorageManager.setUserKeyProtection(/* userId= */ (int) args[0],
+ /* secret= */ (byte[]) args[1]);
+ return null;
+ }).when(sm).setUserKeyProtection(anyInt(), any());
- doAnswer(
- new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) throws Throwable {
- Object[] args = invocation.getArguments();
- mStorageManager.fixateNewestUserKeyAuth((int) args[0] /* userId */);
- return null;
- }
- }).when(sm).fixateNewestUserKeyAuth(anyInt());
return sm;
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java b/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
index 619ef7078c24..91f3fed01267 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
@@ -16,75 +16,26 @@
package com.android.server.locksettings;
-import android.os.IProgressListener;
-import android.os.RemoteException;
-import android.util.ArrayMap;
-
-
-import junit.framework.AssertionFailedError;
+import static com.google.common.truth.Truth.assertThat;
-import java.util.ArrayList;
-import java.util.Arrays;
+import android.util.ArrayMap;
public class FakeStorageManager {
- private ArrayMap<Integer, ArrayList<byte[]>> mAuth = new ArrayMap<>();
- private boolean mIgnoreBadUnlock;
-
- public void addUserKeyAuth(int userId, int serialNumber, byte[] secret) {
- getUserAuth(userId).add(secret);
- }
-
- public void clearUserKeyAuth(int userId, int serialNumber, byte[] secret) {
- ArrayList<byte[]> auths = getUserAuth(userId);
- if (secret == null) {
- return;
- }
- auths.remove(secret);
- auths.add(null);
- }
-
- public void fixateNewestUserKeyAuth(int userId) {
- ArrayList<byte[]> auths = mAuth.get(userId);
- byte[] latest = auths.get(auths.size() - 1);
- auths.clear();
- auths.add(latest);
- }
+ private final ArrayMap<Integer, byte[]> mUserSecrets = new ArrayMap<>();
- private ArrayList<byte[]> getUserAuth(int userId) {
- if (!mAuth.containsKey(userId)) {
- ArrayList<byte[]> auths = new ArrayList<>();
- auths.add(null);
- mAuth.put(userId, auths);
- }
- return mAuth.get(userId);
+ public void setUserKeyProtection(int userId, byte[] secret) {
+ assertThat(mUserSecrets).doesNotContainKey(userId);
+ mUserSecrets.put(userId, secret);
}
public byte[] getUserUnlockToken(int userId) {
- ArrayList<byte[]> auths = getUserAuth(userId);
- if (auths.size() != 1) {
- throw new AssertionFailedError("More than one secret exists");
- }
- return auths.get(0);
- }
-
- public void unlockUser(int userId, byte[] secret, IProgressListener listener)
- throws RemoteException {
- listener.onStarted(userId, null);
- listener.onFinished(userId, null);
- ArrayList<byte[]> auths = getUserAuth(userId);
- if (auths.size() > 1) {
- throw new AssertionFailedError("More than one secret exists");
- }
- byte[] auth = auths.get(0);
- if (!Arrays.equals(secret, auth)) {
- if (!mIgnoreBadUnlock) {
- throw new AssertionFailedError("Invalid secret to unlock user " + userId);
- }
- }
+ byte[] secret = mUserSecrets.get(userId);
+ assertThat(secret).isNotNull();
+ return secret;
}
- public void setIgnoreBadUnlock(boolean ignore) {
- mIgnoreBadUnlock = ignore;
+ public void unlockUserKey(int userId, byte[] secret) {
+ assertThat(mUserSecrets.get(userId)).isEqualTo(secret);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index 3477288f4cff..3f259e343ee6 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -145,15 +145,9 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
// Verify that profile which aren't running (e.g. turn off work) don't get unlocked
assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID));
- /* Currently in LockSettingsService.setLockCredential, unlockUser() is called with the new
- * credential as part of verifyCredential() before the new credential is committed in
- * StorageManager. So we relax the check in our mock StorageManager to allow that.
- */
- mStorageManager.setIgnoreBadUnlock(true);
// Change primary password and verify that profile SID remains
assertTrue(mService.setLockCredential(
secondUnifiedPassword, firstUnifiedPassword, PRIMARY_USER_ID));
- mStorageManager.setIgnoreBadUnlock(false);
assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID));
@@ -172,15 +166,9 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
assertTrue(mService.setLockCredential(primaryPassword,
nonePassword(),
PRIMARY_USER_ID));
- /* Currently in LockSettingsService.setLockCredential, unlockUser() is called with the new
- * credential as part of verifyCredential() before the new credential is committed in
- * StorageManager. So we relax the check in our mock StorageManager to allow that.
- */
- mStorageManager.setIgnoreBadUnlock(true);
assertTrue(mService.setLockCredential(profilePassword,
nonePassword(),
MANAGED_PROFILE_USER_ID));
- mStorageManager.setIgnoreBadUnlock(false);
final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
@@ -203,11 +191,8 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
assertNotNull(mGateKeeperService.getAuthToken(MANAGED_PROFILE_USER_ID));
assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
- // Change primary credential and make sure we don't affect profile
- mStorageManager.setIgnoreBadUnlock(true);
assertTrue(mService.setLockCredential(
newPassword("pwd"), primaryPassword, PRIMARY_USER_ID));
- mStorageManager.setIgnoreBadUnlock(false);
assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
profilePassword, MANAGED_PROFILE_USER_ID, 0 /* flags */)
.getResponseCode());
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 87beece5b414..5e6cccc07983 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -27,6 +27,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
@@ -126,6 +127,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
initializeCredential(password, PRIMARY_USER_ID);
assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
+ verify(mActivityManager).unlockUser2(eq(PRIMARY_USER_ID), any());
assertEquals(VerifyCredentialResponse.RESPONSE_ERROR, mService.verifyCredential(
badPassword, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
@@ -187,32 +189,13 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
}
@Test
- public void testNoSyntheticPasswordOrCredentialDoesNotPassAuthSecret() throws RemoteException {
- mService.onUnlockUser(PRIMARY_USER_ID);
- flushHandlerTasks();
- verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
- }
-
- @Test
- public void testCredentialDoesNotPassAuthSecret() throws RemoteException {
- LockscreenCredential password = newPassword("password");
- initializeCredential(password, PRIMARY_USER_ID);
-
- reset(mAuthSecretService);
- mService.onUnlockUser(PRIMARY_USER_ID);
- flushHandlerTasks();
- verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class));
- }
-
- @Test
- public void testSyntheticPasswordButNoCredentialPassesAuthSecret() throws RemoteException {
+ public void testUnlockUserKeyIfUnsecuredPassesPrimaryUserAuthSecret() throws RemoteException {
LockscreenCredential password = newPassword("password");
initializeCredential(password, PRIMARY_USER_ID);
mService.setLockCredential(nonePassword(), password, PRIMARY_USER_ID);
reset(mAuthSecretService);
- mService.onUnlockUser(PRIMARY_USER_ID);
- flushHandlerTasks();
+ mLocalService.unlockUserKeyIfUnsecured(PRIMARY_USER_ID);
verify(mAuthSecretService).primaryUserCredential(any(ArrayList.class));
}
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
index 1aea6727d3a1..060c31f2985e 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
@@ -16,6 +16,8 @@
package com.android.server.timedetector;
+import static com.android.server.SystemClockTime.TIME_CONFIDENCE_HIGH;
+import static com.android.server.SystemClockTime.TIME_CONFIDENCE_LOW;
import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_EXTERNAL;
import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_GNSS;
import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_NETWORK;
@@ -35,6 +37,7 @@ import android.os.TimestampedValue;
import androidx.test.runner.AndroidJUnit4;
+import com.android.server.SystemClockTime.TimeConfidence;
import com.android.server.timedetector.TimeDetectorStrategy.Origin;
import com.android.server.timezonedetector.ConfigurationChangeListener;
@@ -42,6 +45,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.PrintWriter;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -86,6 +90,7 @@ public class TimeDetectorStrategyImplTest {
.setAutoDetectionSupported(true)
.setSystemClockUpdateThresholdMillis(
ARBITRARY_SYSTEM_CLOCK_UPDATE_THRESHOLD_MILLIS)
+ .setSystemClockUpdateThresholdMillis(TIME_CONFIDENCE_HIGH)
.setAutoSuggestionLowerBound(DEFAULT_SUGGESTION_LOWER_BOUND)
.setManualSuggestionLowerBound(DEFAULT_SUGGESTION_LOWER_BOUND)
.setSuggestionUpperBound(DEFAULT_SUGGESTION_UPPER_BOUND)
@@ -99,6 +104,7 @@ public class TimeDetectorStrategyImplTest {
.setAutoDetectionSupported(true)
.setSystemClockUpdateThresholdMillis(
ARBITRARY_SYSTEM_CLOCK_UPDATE_THRESHOLD_MILLIS)
+ .setSystemClockUpdateThresholdMillis(TIME_CONFIDENCE_HIGH)
.setAutoSuggestionLowerBound(DEFAULT_SUGGESTION_LOWER_BOUND)
.setManualSuggestionLowerBound(DEFAULT_SUGGESTION_LOWER_BOUND)
.setSuggestionUpperBound(DEFAULT_SUGGESTION_UPPER_BOUND)
@@ -112,12 +118,14 @@ public class TimeDetectorStrategyImplTest {
public void setUp() {
mFakeEnvironment = new FakeEnvironment();
mFakeEnvironment.initializeConfig(CONFIG_AUTO_DISABLED);
- mFakeEnvironment.initializeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO);
+ mFakeEnvironment.initializeFakeClocks(
+ ARBITRARY_CLOCK_INITIALIZATION_INFO, TIME_CONFIDENCE_LOW);
}
@Test
public void testSuggestTelephonyTime_autoTimeEnabled() {
- Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED);
+ Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
int slotIndex = ARBITRARY_SLOT_INDEX;
Instant testTime = ARBITRARY_TEST_TIME;
@@ -129,18 +137,21 @@ public class TimeDetectorStrategyImplTest {
long expectedSystemClockMillis =
script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime());
- script.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis)
+ script.verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
+ .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis)
.assertLatestTelephonySuggestion(slotIndex, timeSuggestion);
}
@Test
public void testSuggestTelephonyTime_emptySuggestionIgnored() {
- Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED);
+ Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
int slotIndex = ARBITRARY_SLOT_INDEX;
TelephonyTimeSuggestion timeSuggestion =
script.generateTelephonyTimeSuggestion(slotIndex, null);
script.simulateTelephonyTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking()
.assertLatestTelephonySuggestion(slotIndex, null);
}
@@ -278,17 +289,115 @@ public class TimeDetectorStrategyImplTest {
}
}
+ /**
+ * If an auto suggested time matches the current system clock, the confidence in the current
+ * system clock is raised even when auto time is disabled. The system clock itself must not be
+ * changed.
+ */
@Test
- public void testSuggestTelephonyTime_autoTimeDisabled() {
- Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED);
+ public void testSuggestTelephonyTime_autoTimeDisabled_suggestionMatchesSystemClock() {
+ TimestampedValue<Instant> initialClockTime = ARBITRARY_CLOCK_INITIALIZATION_INFO;
+ final int confidenceUpgradeThresholdMillis = 1000;
+ ConfigurationInternal configInternal =
+ new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED)
+ .setSystemClockConfidenceUpgradeThresholdMillis(
+ confidenceUpgradeThresholdMillis)
+ .build();
+ Script script = new Script()
+ .pokeFakeClocks(initialClockTime, TIME_CONFIDENCE_LOW)
+ .simulateConfigurationInternalChange(configInternal);
int slotIndex = ARBITRARY_SLOT_INDEX;
- TelephonyTimeSuggestion timeSuggestion =
- script.generateTelephonyTimeSuggestion(slotIndex, ARBITRARY_TEST_TIME);
- script.simulateTimePassing()
- .simulateTelephonyTimeSuggestion(timeSuggestion)
- .verifySystemClockWasNotSetAndResetCallTracking()
- .assertLatestTelephonySuggestion(slotIndex, timeSuggestion);
+
+ script.simulateTimePassing();
+ long timeElapsedMillis =
+ script.peekElapsedRealtimeMillis() - initialClockTime.getReferenceTimeMillis();
+
+ // Create a suggestion time that approximately matches the current system clock.
+ Instant suggestionInstant = initialClockTime.getValue()
+ .plusMillis(timeElapsedMillis)
+ .plusMillis(confidenceUpgradeThresholdMillis);
+ TimestampedValue<Long> matchingClockTime = new TimestampedValue<>(
+ script.peekElapsedRealtimeMillis(),
+ suggestionInstant.toEpochMilli());
+ TelephonyTimeSuggestion timeSuggestion = new TelephonyTimeSuggestion.Builder(slotIndex)
+ .setUnixEpochTime(matchingClockTime)
+ .build();
+ script.simulateTelephonyTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
+ .verifySystemClockWasNotSetAndResetCallTracking();
+ }
+
+ /**
+ * If an auto suggested time doesn't match the current system clock, the confidence in the
+ * current system clock will stay where it is. The system clock itself must not be changed.
+ */
+ @Test
+ public void testSuggestTelephonyTime_autoTimeDisabled_suggestionMismatchesSystemClock() {
+ TimestampedValue<Instant> initialClockTime = ARBITRARY_CLOCK_INITIALIZATION_INFO;
+ final int confidenceUpgradeThresholdMillis = 1000;
+ ConfigurationInternal configInternal =
+ new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED)
+ .setSystemClockConfidenceUpgradeThresholdMillis(
+ confidenceUpgradeThresholdMillis)
+ .build();
+ Script script = new Script().pokeFakeClocks(initialClockTime, TIME_CONFIDENCE_LOW)
+ .simulateConfigurationInternalChange(configInternal);
+
+ int slotIndex = ARBITRARY_SLOT_INDEX;
+
+ script.simulateTimePassing();
+ long timeElapsedMillis =
+ script.peekElapsedRealtimeMillis() - initialClockTime.getReferenceTimeMillis();
+
+ // Create a suggestion time that doesn't match the current system clock closely enough.
+ Instant suggestionInstant = initialClockTime.getValue()
+ .plusMillis(timeElapsedMillis)
+ .plusMillis(confidenceUpgradeThresholdMillis + 1);
+ TimestampedValue<Long> mismatchingClockTime = new TimestampedValue<>(
+ script.peekElapsedRealtimeMillis(),
+ suggestionInstant.toEpochMilli());
+ TelephonyTimeSuggestion timeSuggestion = new TelephonyTimeSuggestion.Builder(slotIndex)
+ .setUnixEpochTime(mismatchingClockTime)
+ .build();
+ script.simulateTelephonyTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
+ .verifySystemClockWasNotSetAndResetCallTracking();
+ }
+
+ /**
+ * If a suggested time doesn't match the current system clock, the confidence in the current
+ * system clock will not drop.
+ */
+ @Test
+ public void testSuggestTelephonyTime_autoTimeDisabled_suggestionMismatchesSystemClock2() {
+ TimestampedValue<Instant> initialClockTime = ARBITRARY_CLOCK_INITIALIZATION_INFO;
+ final int confidenceUpgradeThresholdMillis = 1000;
+ ConfigurationInternal configInternal =
+ new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED)
+ .setSystemClockConfidenceUpgradeThresholdMillis(
+ confidenceUpgradeThresholdMillis)
+ .build();
+ Script script = new Script().pokeFakeClocks(initialClockTime, TIME_CONFIDENCE_HIGH)
+ .simulateConfigurationInternalChange(configInternal);
+
+ int slotIndex = ARBITRARY_SLOT_INDEX;
+
+ script.simulateTimePassing();
+ long timeElapsedMillis =
+ script.peekElapsedRealtimeMillis() - initialClockTime.getReferenceTimeMillis();
+
+ // Create a suggestion time that doesn't closely match the current system clock.
+ Instant initialClockInstant = initialClockTime.getValue();
+ TimestampedValue<Long> mismatchingClockTime = new TimestampedValue<>(
+ script.peekElapsedRealtimeMillis(),
+ initialClockInstant.plusMillis(timeElapsedMillis + 1_000_000).toEpochMilli());
+ TelephonyTimeSuggestion timeSuggestion = new TelephonyTimeSuggestion.Builder(slotIndex)
+ .setUnixEpochTime(mismatchingClockTime)
+ .build();
+ script.simulateTelephonyTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
+ .verifySystemClockWasNotSetAndResetCallTracking();
}
@Test
@@ -298,7 +407,8 @@ public class TimeDetectorStrategyImplTest {
new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED)
.setSystemClockUpdateThresholdMillis(systemClockUpdateThresholdMillis)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant testTime = ARBITRARY_TEST_TIME;
int slotIndex = ARBITRARY_SLOT_INDEX;
@@ -311,6 +421,7 @@ public class TimeDetectorStrategyImplTest {
script.simulateTimePassing();
long expectedSystemClockMillis1 = script.calculateTimeInMillisForNow(unixEpochTime1);
script.simulateTelephonyTimeSuggestion(timeSuggestion1)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis1)
.assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);
@@ -327,6 +438,7 @@ public class TimeDetectorStrategyImplTest {
TelephonyTimeSuggestion timeSuggestion2 =
createTelephonyTimeSuggestion(slotIndex, unixEpochTime2);
script.simulateTelephonyTimeSuggestion(timeSuggestion2)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasNotSetAndResetCallTracking()
.assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);
@@ -339,6 +451,7 @@ public class TimeDetectorStrategyImplTest {
TelephonyTimeSuggestion timeSuggestion3 =
createTelephonyTimeSuggestion(slotIndex, unixEpochTime3);
script.simulateTelephonyTimeSuggestion(timeSuggestion3)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasNotSetAndResetCallTracking()
.assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);
@@ -350,6 +463,7 @@ public class TimeDetectorStrategyImplTest {
TelephonyTimeSuggestion timeSuggestion4 =
createTelephonyTimeSuggestion(slotIndex, unixEpochTime4);
script.simulateTelephonyTimeSuggestion(timeSuggestion4)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis4)
.assertLatestTelephonySuggestion(slotIndex, timeSuggestion4);
}
@@ -362,7 +476,8 @@ public class TimeDetectorStrategyImplTest {
new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED)
.setSystemClockUpdateThresholdMillis(systemClockUpdateThresholdMillis)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
int slotIndex = ARBITRARY_SLOT_INDEX;
Instant testTime = ARBITRARY_TEST_TIME;
@@ -376,6 +491,7 @@ public class TimeDetectorStrategyImplTest {
// Simulate the time signal being received. It should not be used because auto time
// detection is off but it should be recorded.
script.simulateTelephonyTimeSuggestion(timeSuggestion1)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking()
.assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);
@@ -386,11 +502,13 @@ public class TimeDetectorStrategyImplTest {
// Turn on auto time detection.
script.simulateAutoTimeDetectionToggle()
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis1)
.assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);
// Turn off auto time detection.
script.simulateAutoTimeDetectionToggle()
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasNotSetAndResetCallTracking()
.assertLatestTelephonySuggestion(slotIndex, timeSuggestion1);
@@ -408,18 +526,21 @@ public class TimeDetectorStrategyImplTest {
// The new time, though valid, should not be set in the system clock because auto time is
// disabled.
script.simulateTelephonyTimeSuggestion(timeSuggestion2)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasNotSetAndResetCallTracking()
.assertLatestTelephonySuggestion(slotIndex, timeSuggestion2);
// Turn on auto time detection.
script.simulateAutoTimeDetectionToggle()
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis2)
.assertLatestTelephonySuggestion(slotIndex, timeSuggestion2);
}
@Test
public void testSuggestTelephonyTime_maxSuggestionAge() {
- Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED);
+ Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
int slotIndex = ARBITRARY_SLOT_INDEX;
Instant testTime = ARBITRARY_TEST_TIME;
@@ -431,6 +552,7 @@ public class TimeDetectorStrategyImplTest {
long expectedSystemClockMillis =
script.calculateTimeInMillisForNow(telephonySuggestion.getUnixEpochTime());
script.simulateTelephonyTimeSuggestion(telephonySuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(
expectedSystemClockMillis /* expectedNetworkBroadcast */)
.assertLatestTelephonySuggestion(slotIndex, telephonySuggestion);
@@ -442,7 +564,7 @@ public class TimeDetectorStrategyImplTest {
script.simulateTimePassing(TimeDetectorStrategyImpl.MAX_SUGGESTION_TIME_AGE_MILLIS);
// Look inside and check what the strategy considers the current best telephony suggestion.
- // It should still be the, it's just no longer used.
+ // It should still be there, it's just no longer used.
assertNull(script.peekBestTelephonySuggestion());
script.assertLatestTelephonySuggestion(slotIndex, telephonySuggestion);
}
@@ -454,12 +576,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_TELEPHONY)
.setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1);
TelephonyTimeSuggestion timeSuggestion =
script.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, belowLowerBound);
script.simulateTelephonyTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -470,12 +594,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_TELEPHONY)
.setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1);
TelephonyTimeSuggestion timeSuggestion =
script.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, aboveLowerBound);
script.simulateTelephonyTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli());
}
@@ -486,12 +612,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_TELEPHONY)
.setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1);
TelephonyTimeSuggestion timeSuggestion =
script.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, aboveUpperBound);
script.simulateTelephonyTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -502,18 +630,21 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_TELEPHONY)
.setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1);
TelephonyTimeSuggestion timeSuggestion =
script.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, belowUpperBound);
script.simulateTelephonyTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli());
}
@Test
public void testSuggestManualTime_autoTimeDisabled() {
- Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED);
+ Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
ManualTimeSuggestion timeSuggestion =
script.generateManualTimeSuggestion(ARBITRARY_TEST_TIME);
@@ -524,12 +655,14 @@ public class TimeDetectorStrategyImplTest {
script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime());
script.simulateManualTimeSuggestion(
ARBITRARY_USER_ID, timeSuggestion, true /* expectedResult */)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
}
@Test
public void testSuggestManualTime_retainsAutoSignal() {
- Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED);
+ Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
int slotIndex = ARBITRARY_SLOT_INDEX;
@@ -544,6 +677,7 @@ public class TimeDetectorStrategyImplTest {
long expectedAutoClockMillis =
script.calculateTimeInMillisForNow(telephonyTimeSuggestion.getUnixEpochTime());
script.simulateTelephonyTimeSuggestion(telephonyTimeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(expectedAutoClockMillis)
.assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion);
@@ -552,6 +686,7 @@ public class TimeDetectorStrategyImplTest {
// Switch to manual.
script.simulateAutoTimeDetectionToggle()
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasNotSetAndResetCallTracking()
.assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion);
@@ -568,6 +703,7 @@ public class TimeDetectorStrategyImplTest {
script.calculateTimeInMillisForNow(manualTimeSuggestion.getUnixEpochTime());
script.simulateManualTimeSuggestion(
ARBITRARY_USER_ID, manualTimeSuggestion, true /* expectedResult */)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(expectedManualClockMillis)
.assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion);
@@ -580,17 +716,20 @@ public class TimeDetectorStrategyImplTest {
expectedAutoClockMillis =
script.calculateTimeInMillisForNow(telephonyTimeSuggestion.getUnixEpochTime());
script.verifySystemClockWasSetAndResetCallTracking(expectedAutoClockMillis)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion);
// Switch back to manual - nothing should happen to the clock.
script.simulateAutoTimeDetectionToggle()
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasNotSetAndResetCallTracking()
.assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion);
}
@Test
public void testSuggestManualTime_isIgnored_whenAutoTimeEnabled() {
- Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED);
+ Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
ManualTimeSuggestion timeSuggestion =
script.generateManualTimeSuggestion(ARBITRARY_TEST_TIME);
@@ -598,6 +737,7 @@ public class TimeDetectorStrategyImplTest {
script.simulateTimePassing()
.simulateManualTimeSuggestion(
ARBITRARY_USER_ID, timeSuggestion, false /* expectedResult */)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -607,12 +747,14 @@ public class TimeDetectorStrategyImplTest {
new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED)
.setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1);
ManualTimeSuggestion timeSuggestion = script.generateManualTimeSuggestion(aboveUpperBound);
script.simulateManualTimeSuggestion(
ARBITRARY_USER_ID, timeSuggestion, false /* expectedResult */)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -622,12 +764,14 @@ public class TimeDetectorStrategyImplTest {
new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED)
.setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1);
ManualTimeSuggestion timeSuggestion = script.generateManualTimeSuggestion(belowUpperBound);
script.simulateManualTimeSuggestion(
ARBITRARY_USER_ID, timeSuggestion, true /* expectedResult */)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli());
}
@@ -637,12 +781,14 @@ public class TimeDetectorStrategyImplTest {
new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED)
.setManualSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1);
ManualTimeSuggestion timeSuggestion = script.generateManualTimeSuggestion(belowLowerBound);
script.simulateManualTimeSuggestion(
ARBITRARY_USER_ID, timeSuggestion, false /* expectedResult */)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -652,12 +798,14 @@ public class TimeDetectorStrategyImplTest {
new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED)
.setManualSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1);
ManualTimeSuggestion timeSuggestion = script.generateManualTimeSuggestion(aboveLowerBound);
script.simulateManualTimeSuggestion(
ARBITRARY_USER_ID, timeSuggestion, true /* expectedResult */)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli());
}
@@ -667,7 +815,8 @@ public class TimeDetectorStrategyImplTest {
new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED)
.setOriginPriorities(ORIGIN_NETWORK)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
NetworkTimeSuggestion timeSuggestion =
script.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME);
@@ -677,6 +826,7 @@ public class TimeDetectorStrategyImplTest {
long expectedSystemClockMillis =
script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime());
script.simulateNetworkTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
}
@@ -703,12 +853,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_NETWORK)
.setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1);
NetworkTimeSuggestion timeSuggestion =
script.generateNetworkTimeSuggestion(belowLowerBound);
script.simulateNetworkTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -719,12 +871,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_NETWORK)
.setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1);
NetworkTimeSuggestion timeSuggestion =
script.generateNetworkTimeSuggestion(aboveLowerBound);
script.simulateNetworkTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli());
}
@@ -735,12 +889,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_NETWORK)
.setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1);
NetworkTimeSuggestion timeSuggestion =
script.generateNetworkTimeSuggestion(aboveUpperBound);
script.simulateNetworkTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -751,12 +907,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_NETWORK)
.setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1);
NetworkTimeSuggestion timeSuggestion =
script.generateNetworkTimeSuggestion(belowUpperBound);
script.simulateNetworkTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli());
}
@@ -766,7 +924,8 @@ public class TimeDetectorStrategyImplTest {
new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED)
.setOriginPriorities(ORIGIN_GNSS)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
GnssTimeSuggestion timeSuggestion =
script.generateGnssTimeSuggestion(ARBITRARY_TEST_TIME);
@@ -776,6 +935,7 @@ public class TimeDetectorStrategyImplTest {
long expectedSystemClockMillis =
script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime());
script.simulateGnssTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
}
@@ -802,12 +962,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_GNSS)
.setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1);
GnssTimeSuggestion timeSuggestion =
script.generateGnssTimeSuggestion(belowLowerBound);
script.simulateGnssTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -818,12 +980,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_GNSS)
.setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1);
GnssTimeSuggestion timeSuggestion =
script.generateGnssTimeSuggestion(aboveLowerBound);
script.simulateGnssTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli());
}
@@ -834,12 +998,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_GNSS)
.setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1);
GnssTimeSuggestion timeSuggestion =
script.generateGnssTimeSuggestion(aboveUpperBound);
script.simulateGnssTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -850,12 +1016,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_GNSS)
.setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1);
GnssTimeSuggestion timeSuggestion =
script.generateGnssTimeSuggestion(belowUpperBound);
script.simulateGnssTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli());
}
@@ -865,7 +1033,8 @@ public class TimeDetectorStrategyImplTest {
new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED)
.setOriginPriorities(ORIGIN_EXTERNAL)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
ExternalTimeSuggestion timeSuggestion =
script.generateExternalTimeSuggestion(ARBITRARY_TEST_TIME);
@@ -875,6 +1044,7 @@ public class TimeDetectorStrategyImplTest {
long expectedSystemClockMillis =
script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime());
script.simulateExternalTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis);
}
@@ -901,12 +1071,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_EXTERNAL)
.setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1);
ExternalTimeSuggestion timeSuggestion =
script.generateExternalTimeSuggestion(belowLowerBound);
script.simulateExternalTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -917,12 +1089,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_EXTERNAL)
.setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1);
ExternalTimeSuggestion timeSuggestion =
script.generateExternalTimeSuggestion(aboveLowerBound);
script.simulateExternalTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli());
}
@@ -933,12 +1107,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_EXTERNAL)
.setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1);
ExternalTimeSuggestion timeSuggestion =
script.generateExternalTimeSuggestion(aboveUpperBound);
script.simulateExternalTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW)
.verifySystemClockWasNotSetAndResetCallTracking();
}
@@ -949,12 +1125,14 @@ public class TimeDetectorStrategyImplTest {
.setOriginPriorities(ORIGIN_EXTERNAL)
.setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND)
.build();
- Script script = new Script().simulateConfigurationInternalChange(configInternal);
+ Script script = new Script().simulateConfigurationInternalChange(configInternal)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_LOW);
Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1);
ExternalTimeSuggestion timeSuggestion =
script.generateExternalTimeSuggestion(belowUpperBound);
script.simulateExternalTimeSuggestion(timeSuggestion)
+ .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH)
.verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli());
}
@@ -1472,6 +1650,7 @@ public class TimeDetectorStrategyImplTest {
private boolean mWakeLockAcquired;
private long mElapsedRealtimeMillis;
private long mSystemClockMillis;
+ private int mSystemClockConfidence = TIME_CONFIDENCE_LOW;
private ConfigurationChangeListener mConfigurationInternalChangeListener;
// Tracking operations.
@@ -1481,9 +1660,10 @@ public class TimeDetectorStrategyImplTest {
mConfigurationInternal = configurationInternal;
}
- public void initializeFakeClocks(TimestampedValue<Instant> timeInfo) {
+ public void initializeFakeClocks(
+ TimestampedValue<Instant> timeInfo, @TimeConfidence int timeConfidence) {
pokeElapsedRealtimeMillis(timeInfo.getReferenceTimeMillis());
- pokeSystemClockMillis(timeInfo.getValue().toEpochMilli());
+ pokeSystemClockMillis(timeInfo.getValue().toEpochMilli(), timeConfidence);
}
@Override
@@ -1515,10 +1695,23 @@ public class TimeDetectorStrategyImplTest {
}
@Override
- public void setSystemClock(long newTimeMillis) {
+ public @TimeConfidence int systemClockConfidence() {
+ return mSystemClockConfidence;
+ }
+
+ @Override
+ public void setSystemClock(
+ long newTimeMillis, @TimeConfidence int confidence, String logMsg) {
assertWakeLockAcquired();
mSystemClockWasSet = true;
mSystemClockMillis = newTimeMillis;
+ mSystemClockConfidence = confidence;
+ }
+
+ @Override
+ public void setSystemClockConfidence(@TimeConfidence int confidence, String logMsg) {
+ assertWakeLockAcquired();
+ mSystemClockConfidence = confidence;
}
@Override
@@ -1527,6 +1720,16 @@ public class TimeDetectorStrategyImplTest {
mWakeLockAcquired = false;
}
+ @Override
+ public void addDebugLogEntry(String logMsg) {
+ // No-op for tests
+ }
+
+ @Override
+ public void dumpDebugLog(PrintWriter printWriter) {
+ // No-op for tests
+ }
+
// Methods below are for managing the fake's behavior.
void simulateConfigurationInternalChange(ConfigurationInternal configurationInternal) {
@@ -1538,8 +1741,9 @@ public class TimeDetectorStrategyImplTest {
mElapsedRealtimeMillis = elapsedRealtimeMillis;
}
- void pokeSystemClockMillis(long systemClockMillis) {
+ void pokeSystemClockMillis(long systemClockMillis, @TimeConfidence int timeConfidence) {
mSystemClockMillis = systemClockMillis;
+ mSystemClockConfidence = timeConfidence;
}
long peekElapsedRealtimeMillis() {
@@ -1567,6 +1771,10 @@ public class TimeDetectorStrategyImplTest {
assertEquals(expectedSystemClockMillis, mSystemClockMillis);
}
+ public void verifySystemClockConfidence(@TimeConfidence int expectedConfidence) {
+ assertEquals(expectedConfidence, mSystemClockConfidence);
+ }
+
void resetCallTracking() {
mSystemClockWasSet = false;
}
@@ -1589,6 +1797,14 @@ public class TimeDetectorStrategyImplTest {
mTimeDetectorStrategy = new TimeDetectorStrategyImpl(mFakeEnvironment);
}
+ Script pokeFakeClocks(TimestampedValue<Instant> initialClockTime,
+ @TimeConfidence int timeConfidence) {
+ mFakeEnvironment.pokeElapsedRealtimeMillis(initialClockTime.getReferenceTimeMillis());
+ mFakeEnvironment.pokeSystemClockMillis(
+ initialClockTime.getValue().toEpochMilli(), timeConfidence);
+ return this;
+ }
+
long peekElapsedRealtimeMillis() {
return mFakeEnvironment.peekElapsedRealtimeMillis();
}
@@ -1675,6 +1891,11 @@ public class TimeDetectorStrategyImplTest {
return this;
}
+ Script verifySystemClockConfidence(@TimeConfidence int expectedConfidence) {
+ mFakeEnvironment.verifySystemClockConfidence(expectedConfidence);
+ return this;
+ }
+
/**
* White box test info: Asserts the latest suggestion for the slotIndex is as expected.
*/
diff --git a/telephony/common/com/android/internal/telephony/SmsApplication.java b/telephony/common/com/android/internal/telephony/SmsApplication.java
index 7b7c83f39c92..f848c4013fa9 100644
--- a/telephony/common/com/android/internal/telephony/SmsApplication.java
+++ b/telephony/common/com/android/internal/telephony/SmsApplication.java
@@ -190,12 +190,11 @@ public final class SmsApplication {
}
/**
- * Returns the userId of the Context object, if called from a system app,
+ * Returns the userId of the current process, if called from a system app,
* otherwise it returns the caller's userId
- * @param context The context object passed in by the caller.
- * @return
+ * @return userId of the caller.
*/
- private static int getIncomingUserId(Context context) {
+ private static int getIncomingUserId() {
int contextUserId = UserHandle.myUserId();
final int callingUid = Binder.getCallingUid();
if (DEBUG_MULTIUSER) {
@@ -231,7 +230,7 @@ public final class SmsApplication {
*/
@UnsupportedAppUsage
public static Collection<SmsApplicationData> getApplicationCollection(Context context) {
- return getApplicationCollectionAsUser(context, getIncomingUserId(context));
+ return getApplicationCollectionAsUser(context, getIncomingUserId());
}
/**
@@ -590,7 +589,7 @@ public final class SmsApplication {
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static void setDefaultApplication(String packageName, Context context) {
- setDefaultApplicationAsUser(packageName, context, getIncomingUserId(context));
+ setDefaultApplicationAsUser(packageName, context, getIncomingUserId());
}
/**
@@ -952,7 +951,7 @@ public final class SmsApplication {
*/
@UnsupportedAppUsage
public static ComponentName getDefaultSmsApplication(Context context, boolean updateIfNeeded) {
- return getDefaultSmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId(context));
+ return getDefaultSmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
}
/**
@@ -988,7 +987,18 @@ public final class SmsApplication {
*/
@UnsupportedAppUsage
public static ComponentName getDefaultMmsApplication(Context context, boolean updateIfNeeded) {
- int userId = getIncomingUserId(context);
+ return getDefaultMmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
+ }
+
+ /**
+ * Gets the default MMS application on a given user
+ * @param context context from the calling app
+ * @param updateIfNeeded update the default app if there is no valid default app configured.
+ * @param userId target user ID.
+ * @return component name of the app and class to deliver MMS messages to.
+ */
+ public static ComponentName getDefaultMmsApplicationAsUser(Context context,
+ boolean updateIfNeeded, int userId) {
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1013,7 +1023,19 @@ public final class SmsApplication {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static ComponentName getDefaultRespondViaMessageApplication(Context context,
boolean updateIfNeeded) {
- int userId = getIncomingUserId(context);
+ return getDefaultRespondViaMessageApplicationAsUser(context, updateIfNeeded,
+ getIncomingUserId());
+ }
+
+ /**
+ * Gets the default Respond Via Message application on a given user
+ * @param context context from the calling app
+ * @param updateIfNeeded update the default app if there is no valid default app configured
+ * @param userId target user ID.
+ * @return component name of the app and class to direct Respond Via Message intent to
+ */
+ public static ComponentName getDefaultRespondViaMessageApplicationAsUser(Context context,
+ boolean updateIfNeeded, int userId) {
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1039,7 +1061,7 @@ public final class SmsApplication {
*/
public static ComponentName getDefaultSendToApplication(Context context,
boolean updateIfNeeded) {
- int userId = getIncomingUserId(context);
+ int userId = getIncomingUserId();
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1064,7 +1086,20 @@ public final class SmsApplication {
*/
public static ComponentName getDefaultExternalTelephonyProviderChangedApplication(
Context context, boolean updateIfNeeded) {
- int userId = getIncomingUserId(context);
+ return getDefaultExternalTelephonyProviderChangedApplicationAsUser(context, updateIfNeeded,
+ getIncomingUserId());
+ }
+
+ /**
+ * Gets the default application that handles external changes to the SmsProvider and
+ * MmsProvider on a given user.
+ * @param context context from the calling app
+ * @param updateIfNeeded update the default app if there is no valid default app configured
+ * @param userId target user ID.
+ * @return component name of the app and class to deliver change intents to.
+ */
+ public static ComponentName getDefaultExternalTelephonyProviderChangedApplicationAsUser(
+ Context context, boolean updateIfNeeded, int userId) {
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1089,7 +1124,18 @@ public final class SmsApplication {
*/
public static ComponentName getDefaultSimFullApplication(
Context context, boolean updateIfNeeded) {
- int userId = getIncomingUserId(context);
+ return getDefaultSimFullApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
+ }
+
+ /**
+ * Gets the default application that handles sim full event on a given user.
+ * @param context context from the calling app
+ * @param updateIfNeeded update the default app if there is no valid default app configured
+ * @param userId target user ID.
+ * @return component name of the app and class to deliver change intents to
+ */
+ public static ComponentName getDefaultSimFullApplicationAsUser(Context context,
+ boolean updateIfNeeded, int userId) {
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1114,7 +1160,12 @@ public final class SmsApplication {
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static boolean shouldWriteMessageForPackage(String packageName, Context context) {
- return !isDefaultSmsApplication(context, packageName);
+ return !shouldWriteMessageForPackageAsUser(packageName, context, getIncomingUserId());
+ }
+
+ public static boolean shouldWriteMessageForPackageAsUser(String packageName, Context context,
+ int userId) {
+ return !isDefaultSmsApplicationAsUser(context, packageName, userId);
}
/**
@@ -1126,28 +1177,42 @@ public final class SmsApplication {
*/
@UnsupportedAppUsage
public static boolean isDefaultSmsApplication(Context context, String packageName) {
+ return isDefaultSmsApplicationAsUser(context, packageName, getIncomingUserId());
+ }
+
+ /**
+ * Check if a package is default sms app (or equivalent, like bluetooth) on a given user.
+ *
+ * @param context context from the calling app
+ * @param packageName the name of the package to be checked
+ * @param userId target user ID.
+ * @return true if the package is default sms app or bluetooth
+ */
+ public static boolean isDefaultSmsApplicationAsUser(Context context, String packageName,
+ int userId) {
if (packageName == null) {
return false;
}
- final String defaultSmsPackage = getDefaultSmsApplicationPackageName(context);
- final String bluetoothPackageName = context.getResources()
+ ComponentName component = getDefaultSmsApplicationAsUser(context, false,
+ userId);
+ if (component == null) {
+ return false;
+ }
+
+ String defaultSmsPackage = component.getPackageName();
+ if (defaultSmsPackage == null) {
+ return false;
+ }
+
+ String bluetoothPackageName = context.getResources()
.getString(com.android.internal.R.string.config_systemBluetoothStack);
- if ((defaultSmsPackage != null && defaultSmsPackage.equals(packageName))
- || bluetoothPackageName.equals(packageName)) {
+ if (defaultSmsPackage.equals(packageName) || bluetoothPackageName.equals(packageName)) {
return true;
}
return false;
}
- private static String getDefaultSmsApplicationPackageName(Context context) {
- final ComponentName component = getDefaultSmsApplication(context, false);
- if (component != null) {
- return component.getPackageName();
- }
- return null;
- }
-
/**
* Check if a package is default mms app (or equivalent, like bluetooth)
*
@@ -1157,25 +1222,40 @@ public final class SmsApplication {
*/
@UnsupportedAppUsage
public static boolean isDefaultMmsApplication(Context context, String packageName) {
+ return isDefaultMmsApplicationAsUser(context, packageName, getIncomingUserId());
+ }
+
+ /**
+ * Check if a package is default mms app (or equivalent, like bluetooth) on a given user.
+ *
+ * @param context context from the calling app
+ * @param packageName the name of the package to be checked
+ * @param userId target user ID.
+ * @return true if the package is default mms app or bluetooth
+ */
+ public static boolean isDefaultMmsApplicationAsUser(Context context, String packageName,
+ int userId) {
if (packageName == null) {
return false;
}
- String defaultMmsPackage = getDefaultMmsApplicationPackageName(context);
+
+ ComponentName component = getDefaultMmsApplicationAsUser(context, false,
+ userId);
+ if (component == null) {
+ return false;
+ }
+
+ String defaultMmsPackage = component.getPackageName();
+ if (defaultMmsPackage == null) {
+ return false;
+ }
+
String bluetoothPackageName = context.getResources()
.getString(com.android.internal.R.string.config_systemBluetoothStack);
- if ((defaultMmsPackage != null && defaultMmsPackage.equals(packageName))
- || bluetoothPackageName.equals(packageName)) {
+ if (defaultMmsPackage.equals(packageName)|| bluetoothPackageName.equals(packageName)) {
return true;
}
return false;
}
-
- private static String getDefaultMmsApplicationPackageName(Context context) {
- ComponentName component = getDefaultMmsApplication(context, false);
- if (component != null) {
- return component.getPackageName();
- }
- return null;
- }
-}
+} \ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java
new file mode 100644
index 000000000000..858cd7672fe3
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java
@@ -0,0 +1,224 @@
+/*
+ * 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.wm.flicker.helpers;
+
+import android.annotation.NonNull;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.os.SystemClock;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+/**
+ * Injects gestures given an {@link Instrumentation} object.
+ */
+public class GestureHelper {
+ // Inserted after each motion event injection.
+ private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
+
+ private final UiAutomation mUiAutomation;
+
+ /**
+ * A pair of floating point values.
+ */
+ public static class Tuple {
+ public float x;
+ public float y;
+
+ public Tuple(float x, float y) {
+ this.x = x;
+ this.y = y;
+ }
+ }
+
+ public GestureHelper(Instrumentation instrumentation) {
+ mUiAutomation = instrumentation.getUiAutomation();
+ }
+
+ /**
+ * Injects a series of {@link MotionEvent} objects to simulate a pinch gesture.
+ *
+ * @param startPoint1 initial coordinates of the first pointer
+ * @param startPoint2 initial coordinates of the second pointer
+ * @param endPoint1 final coordinates of the first pointer
+ * @param endPoint2 final coordinates of the second pointer
+ * @param steps number of steps to take to animate pinching
+ * @return true if gesture is injected successfully
+ */
+ public boolean pinch(@NonNull Tuple startPoint1, @NonNull Tuple startPoint2,
+ @NonNull Tuple endPoint1, @NonNull Tuple endPoint2, int steps) {
+ PointerProperties ptrProp1 = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER);
+ PointerProperties ptrProp2 = getPointerProp(1, MotionEvent.TOOL_TYPE_FINGER);
+
+ PointerCoords ptrCoord1 = getPointerCoord(startPoint1.x, startPoint1.y, 1, 1);
+ PointerCoords ptrCoord2 = getPointerCoord(startPoint2.x, startPoint2.y, 1, 1);
+
+ PointerProperties[] ptrProps = new PointerProperties[] {
+ ptrProp1, ptrProp2
+ };
+
+ PointerCoords[] ptrCoords = new PointerCoords[] {
+ ptrCoord1, ptrCoord2
+ };
+
+ long downTime = SystemClock.uptimeMillis();
+
+ if (!primaryPointerDown(ptrProp1, ptrCoord1, downTime)) {
+ return false;
+ }
+
+ if (!nonPrimaryPointerDown(ptrProps, ptrCoords, downTime, 1)) {
+ return false;
+ }
+
+ if (!movePointers(ptrProps, ptrCoords, new Tuple[] { endPoint1, endPoint2 },
+ downTime, steps)) {
+ return false;
+ }
+
+ if (!nonPrimaryPointerUp(ptrProps, ptrCoords, downTime, 1)) {
+ return false;
+ }
+
+ return primaryPointerUp(ptrProp1, ptrCoord1, downTime);
+ }
+
+ private boolean primaryPointerDown(@NonNull PointerProperties prop,
+ @NonNull PointerCoords coord, long downTime) {
+ MotionEvent event = getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, 1,
+ new PointerProperties[]{ prop }, new PointerCoords[]{ coord });
+
+ return injectEventSync(event);
+ }
+
+ private boolean nonPrimaryPointerDown(@NonNull PointerProperties[] props,
+ @NonNull PointerCoords[] coords, long downTime, int index) {
+ // at least 2 pointers are needed
+ if (props.length != coords.length || coords.length < 2) {
+ return false;
+ }
+
+ long eventTime = SystemClock.uptimeMillis();
+
+ MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_DOWN
+ + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords);
+
+ return injectEventSync(event);
+ }
+
+ private boolean movePointers(@NonNull PointerProperties[] props,
+ @NonNull PointerCoords[] coords, @NonNull Tuple[] endPoints, long downTime, int steps) {
+ // the number of endpoints should be the same as the number of pointers
+ if (props.length != coords.length || coords.length != endPoints.length) {
+ return false;
+ }
+
+ // prevent division by 0 and negative number of steps
+ if (steps < 1) {
+ steps = 1;
+ }
+
+ // save the starting points before updating any pointers
+ Tuple[] startPoints = new Tuple[coords.length];
+
+ for (int i = 0; i < coords.length; i++) {
+ startPoints[i] = new Tuple(coords[i].x, coords[i].y);
+ }
+
+ MotionEvent event;
+ long eventTime;
+
+ for (int i = 0; i < steps; i++) {
+ // inject a delay between movements
+ SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
+
+ // update the coordinates
+ for (int j = 0; j < coords.length; j++) {
+ coords[j].x += (endPoints[j].x - startPoints[j].x) / steps;
+ coords[j].y += (endPoints[j].y - startPoints[j].y) / steps;
+ }
+
+ eventTime = SystemClock.uptimeMillis();
+
+ event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_MOVE,
+ coords.length, props, coords);
+
+ boolean didInject = injectEventSync(event);
+
+ if (!didInject) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean primaryPointerUp(@NonNull PointerProperties prop,
+ @NonNull PointerCoords coord, long downTime) {
+ long eventTime = SystemClock.uptimeMillis();
+
+ MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_UP, 1,
+ new PointerProperties[]{ prop }, new PointerCoords[]{ coord });
+
+ return injectEventSync(event);
+ }
+
+ private boolean nonPrimaryPointerUp(@NonNull PointerProperties[] props,
+ @NonNull PointerCoords[] coords, long downTime, int index) {
+ // at least 2 pointers are needed
+ if (props.length != coords.length || coords.length < 2) {
+ return false;
+ }
+
+ long eventTime = SystemClock.uptimeMillis();
+
+ MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_UP
+ + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords);
+
+ return injectEventSync(event);
+ }
+
+ private PointerCoords getPointerCoord(float x, float y, float pressure, float size) {
+ PointerCoords ptrCoord = new PointerCoords();
+ ptrCoord.x = x;
+ ptrCoord.y = y;
+ ptrCoord.pressure = pressure;
+ ptrCoord.size = size;
+ return ptrCoord;
+ }
+
+ private PointerProperties getPointerProp(int id, int toolType) {
+ PointerProperties ptrProp = new PointerProperties();
+ ptrProp.id = id;
+ ptrProp.toolType = toolType;
+ return ptrProp;
+ }
+
+ private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
+ int pointerCount, PointerProperties[] ptrProps, PointerCoords[] ptrCoords) {
+ return MotionEvent.obtain(downTime, eventTime, action, pointerCount,
+ ptrProps, ptrCoords, 0, 0, 1.0f, 1.0f,
+ 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+ }
+
+ private boolean injectEventSync(InputEvent event) {
+ return mUiAutomation.injectInputEvent(event, true);
+ }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index e730f315392b..19ee09a81ec5 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -22,6 +22,7 @@ import android.media.session.MediaSessionManager
import android.util.Log
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.GestureHelper.Tuple
import com.android.server.wm.flicker.testapp.ActivityOptions
import com.android.server.wm.traces.common.Rect
import com.android.server.wm.traces.common.WindowManagerConditionsFactory
@@ -44,6 +45,8 @@ open class PipAppHelper(instrumentation: Instrumentation) :
get() =
mediaSessionManager.getActiveSessions(null).firstOrNull { it.packageName == `package` }
+ private val gestureHelper: GestureHelper = GestureHelper(mInstrumentation)
+
open fun clickObject(resId: String) {
val selector = By.res(`package`, resId)
val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object")
@@ -52,6 +55,50 @@ open class PipAppHelper(instrumentation: Instrumentation) :
}
/**
+ * Expands the PIP window my using the pinch out gesture.
+ *
+ * @param percent The percentage by which to increase the pip window size.
+ * @throws IllegalArgumentException if percentage isn't between 0.0f and 1.0f
+ */
+ fun pinchOpenPipWindow(wmHelper: WindowManagerStateHelper, percent: Float, steps: Int) {
+ // the percentage must be between 0.0f and 1.0f
+ if (percent <= 0.0f || percent > 1.0f) {
+ throw IllegalArgumentException("Percent must be between 0.0f and 1.0f")
+ }
+
+ val windowRect = getWindowRect(wmHelper)
+
+ // first pointer's initial x coordinate is halfway between the left edge and the center
+ val initLeftX = (windowRect.centerX() - windowRect.width / 4).toFloat()
+ // second pointer's initial x coordinate is halfway between the right edge and the center
+ val initRightX = (windowRect.centerX() + windowRect.width / 4).toFloat()
+
+ // horizontal distance the window should increase by
+ val distIncrease = windowRect.width * percent
+
+ // final x-coordinates
+ val finalLeftX = initLeftX - (distIncrease / 2)
+ val finalRightX = initRightX + (distIncrease / 2)
+
+ // y-coordinate is the same throughout this animation
+ val yCoord = windowRect.centerY().toFloat()
+
+ var adjustedSteps = MIN_STEPS_TO_ANIMATE
+
+ // if distance per step is at least 1, then we can use the number of steps requested
+ if (distIncrease.toInt() / (steps * 2) >= 1) {
+ adjustedSteps = steps
+ }
+
+ // if the distance per step is less than 1, carry out the animation in two steps
+ gestureHelper.pinch(
+ Tuple(initLeftX, yCoord), Tuple(initRightX, yCoord),
+ Tuple(finalLeftX, yCoord), Tuple(finalRightX, yCoord), adjustedSteps)
+
+ waitForPipWindowToExpandFrom(wmHelper, Region.from(windowRect))
+ }
+
+ /**
* Launches the app through an intent instead of interacting with the launcher and waits until
* the app window is in PIP mode
*/
@@ -194,5 +241,8 @@ open class PipAppHelper(instrumentation: Instrumentation) :
private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start"
private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual"
private const val ENTER_PIP_AUTOENTER = "enter_pip_on_leave_autoenter"
+ // minimum number of steps to take, when animating gestures, needs to be 2
+ // so that there is at least a single intermediate layer that flicker tests can check
+ private const val MIN_STEPS_TO_ANIMATE = 2
}
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
index d80cf4ee619c..80ab01624703 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
@@ -16,7 +16,6 @@
package com.android.server.wm.flicker.ime
-import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.FlickerParametersRunnerFactory
@@ -47,21 +46,21 @@ class SwitchImeWindowsFromGestureNavTest_ShellTransit(testSpec: FlickerTestParam
Assume.assumeTrue(isShellTransitionsEnabled)
}
- @FlakyTest(bugId = 228012334)
+ @Presubmit
@Test
override fun entireScreenCovered() = super.entireScreenCovered()
- @FlakyTest(bugId = 228012334)
+ @Presubmit
@Test
override fun imeLayerIsVisibleWhenSwitchingToImeApp() =
super.imeLayerIsVisibleWhenSwitchingToImeApp()
- @FlakyTest(bugId = 228012334)
+ @Presubmit
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
- @FlakyTest(bugId = 228012334)
+ @Presubmit
@Test
override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
super.visibleWindowsShownMoreThanOneConsecutiveEntry()
diff --git a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
index 052ce3a902c1..7a2af72c41d5 100644
--- a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
+++ b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
@@ -153,6 +153,20 @@ public class SmsApplicationTest {
}
@Test
+ public void testGetDefaultMmsApplication() {
+ assertEquals(TEST_COMPONENT_NAME,
+ SmsApplication.getDefaultMmsApplicationAsUser(mContext, false,
+ UserHandle.USER_SYSTEM));
+ }
+
+ @Test
+ public void testGetDefaultExternalTelephonyProviderChangedApplication() {
+ assertEquals(TEST_COMPONENT_NAME,
+ SmsApplication.getDefaultExternalTelephonyProviderChangedApplicationAsUser(mContext,
+ false, UserHandle.USER_SYSTEM));
+ }
+
+ @Test
public void testGetDefaultSmsApplicationWithAppOpsFix() throws Exception {
when(mAppOpsManager.unsafeCheckOp(AppOpsManager.OPSTR_READ_SMS, SMS_APP_UID,
TEST_COMPONENT_NAME.getPackageName()))
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
index 8aa3e2583071..4d69d26e46db 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
@@ -42,6 +42,8 @@ class AndroidFrameworkIssueRegistry : IssueRegistry() {
SaferParcelChecker.ISSUE_UNSAFE_API_USAGE,
PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
RegisterReceiverFlagDetector.ISSUE_RECEIVER_EXPORTED_FLAG,
+ PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE,
+ PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD,
)
override val api: Int
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt
index 82eb8ed8f621..3d5d01c9b7a0 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt
@@ -34,3 +34,7 @@ val ENFORCE_PERMISSION_METHODS = listOf(
Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"),
Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission")
)
+
+const val ANNOTATION_PERMISSION_METHOD = "android.content.pm.PermissionMethod"
+const val ANNOTATION_PERMISSION_NAME = "android.content.pm.PermissionName"
+const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult"
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
new file mode 100644
index 000000000000..68a450d956a8
--- /dev/null
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
@@ -0,0 +1,201 @@
+/*
+ * 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.google.android.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.getUMethod
+import com.intellij.psi.PsiType
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.getContainingUMethod
+
+/**
+ * Stops incorrect usage of {@link PermissionMethod}
+ * TODO: add tests once re-enabled (b/240445172, b/247542171)
+ */
+class PermissionMethodDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableUastTypes(): List<Class<out UElement>> =
+ listOf(UAnnotation::class.java, UMethod::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler =
+ PermissionMethodHandler(context)
+
+ private inner class PermissionMethodHandler(val context: JavaContext) : UElementHandler() {
+ override fun visitMethod(node: UMethod) {
+ if (hasPermissionMethodAnnotation(node)) return
+ if (onlyCallsPermissionMethod(node)) {
+ val location = context.getLocation(node.javaPsi.modifierList)
+ val fix = fix()
+ .annotate(ANNOTATION_PERMISSION_METHOD)
+ .range(location)
+ .autoFix()
+ .build()
+
+ context.report(
+ ISSUE_CAN_BE_PERMISSION_METHOD,
+ location,
+ "Annotate method with @PermissionMethod",
+ fix
+ )
+ }
+ }
+
+ override fun visitAnnotation(node: UAnnotation) {
+ if (node.qualifiedName != ANNOTATION_PERMISSION_METHOD) return
+ val method = node.getContainingUMethod() ?: return
+
+ if (!isPermissionMethodReturnType(method)) {
+ context.report(
+ ISSUE_PERMISSION_METHOD_USAGE,
+ context.getLocation(node),
+ """
+ Methods annotated with `@PermissionMethod` should return `void`, \
+ `boolean`, or `@PackageManager.PermissionResult int`."
+ """.trimIndent()
+ )
+ }
+
+ if (method.returnType == PsiType.INT &&
+ method.annotations.none { it.hasQualifiedName(ANNOTATION_PERMISSION_RESULT) }
+ ) {
+ context.report(
+ ISSUE_PERMISSION_METHOD_USAGE,
+ context.getLocation(node),
+ """
+ Methods annotated with `@PermissionMethod` that return `int` should \
+ also be annotated with `@PackageManager.PermissionResult.`"
+ """.trimIndent()
+ )
+ }
+ }
+ }
+
+ companion object {
+
+ private val EXPLANATION_PERMISSION_METHOD_USAGE = """
+ `@PermissionMethod` should annotate methods that ONLY perform permission lookups. \
+ Said methods should return `boolean`, `@PackageManager.PermissionResult int`, or return \
+ `void` and potentially throw `SecurityException`.
+ """.trimIndent()
+
+ @JvmField
+ val ISSUE_PERMISSION_METHOD_USAGE = Issue.create(
+ id = "PermissionMethodUsage",
+ briefDescription = "@PermissionMethod used incorrectly",
+ explanation = EXPLANATION_PERMISSION_METHOD_USAGE,
+ category = Category.CORRECTNESS,
+ priority = 5,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ PermissionMethodDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ ),
+ enabledByDefault = true
+ )
+
+ private val EXPLANATION_CAN_BE_PERMISSION_METHOD = """
+ Methods that only call other methods annotated with @PermissionMethod (and do NOTHING else) can themselves \
+ be annotated with @PermissionMethod. For example:
+ ```
+ void wrapperHelper() {
+ // Context.enforceCallingPermission is annotated with @PermissionMethod
+ context.enforceCallingPermission(SOME_PERMISSION)
+ }
+ ```
+ """.trimIndent()
+
+ @JvmField
+ val ISSUE_CAN_BE_PERMISSION_METHOD = Issue.create(
+ id = "CanBePermissionMethod",
+ briefDescription = "Method can be annotated with @PermissionMethod",
+ explanation = EXPLANATION_CAN_BE_PERMISSION_METHOD,
+ category = Category.SECURITY,
+ priority = 5,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ PermissionMethodDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ ),
+ enabledByDefault = false
+ )
+
+ private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
+ .any {
+ it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD)
+ }
+
+ private fun isPermissionMethodReturnType(method: UMethod): Boolean =
+ listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType)
+
+ /**
+ * Identifies methods that...
+ * DO call other methods annotated with @PermissionMethod
+ * DO NOT do anything else
+ */
+ private fun onlyCallsPermissionMethod(method: UMethod): Boolean {
+ val body = method.uastBody as? UBlockExpression ?: return false
+ if (body.expressions.isEmpty()) return false
+ for (expression in body.expressions) {
+ when (expression) {
+ is UQualifiedReferenceExpression -> {
+ if (!isPermissionMethodCall(expression.selector)) return false
+ }
+ is UReturnExpression -> {
+ if (!isPermissionMethodCall(expression.returnExpression)) return false
+ }
+ is UCallExpression -> {
+ if (!isPermissionMethodCall(expression)) return false
+ }
+ is UIfExpression -> {
+ if (expression.thenExpression !is UReturnExpression) return false
+ if (!isPermissionMethodCall(expression.condition)) return false
+ }
+ else -> return false
+ }
+ }
+ return true
+ }
+
+ private fun isPermissionMethodCall(expression: UExpression?): Boolean {
+ return when (expression) {
+ is UQualifiedReferenceExpression ->
+ return isPermissionMethodCall(expression.selector)
+ is UCallExpression -> {
+ val calledMethod = expression.resolve()?.getUMethod() ?: return false
+ return hasPermissionMethodAnnotation(calledMethod)
+ }
+ else -> false
+ }
+ }
+ }
+}
diff --git a/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt b/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
index 3ab09a8366be..dfebdccf1d63 100644
--- a/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
+++ b/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
@@ -37,10 +37,11 @@ val IMMUTABLE_ANNOTATION_NAME = Immutable::class.qualifiedName
class ImmutabilityProcessor : AbstractProcessor() {
companion object {
+
/**
- * Types that are already immutable.
+ * Types that are already immutable. Will also ignore subclasses.
*/
- private val IGNORED_TYPES = listOf(
+ private val IGNORED_SUPER_TYPES = listOf(
"java.io.File",
"java.lang.Boolean",
"java.lang.Byte",
@@ -56,6 +57,15 @@ class ImmutabilityProcessor : AbstractProcessor() {
"android.os.Parcelable.Creator",
)
+ /**
+ * Types that are already immutable. Must be an exact match, does not include any super
+ * or sub classes.
+ */
+ private val IGNORED_EXACT_TYPES = listOf(
+ "java.lang.Class",
+ "java.lang.Object",
+ )
+
private val IGNORED_METHODS = listOf(
"writeToParcel",
)
@@ -64,7 +74,8 @@ class ImmutabilityProcessor : AbstractProcessor() {
private lateinit var collectionType: TypeMirror
private lateinit var mapType: TypeMirror
- private lateinit var ignoredTypes: List<TypeMirror>
+ private lateinit var ignoredSuperTypes: List<TypeMirror>
+ private lateinit var ignoredExactTypes: List<TypeMirror>
private val seenTypesByPolicy = mutableMapOf<Set<Immutable.Policy.Exception>, Set<Type>>()
@@ -76,7 +87,8 @@ class ImmutabilityProcessor : AbstractProcessor() {
super.init(processingEnv)
collectionType = processingEnv.erasedType("java.util.Collection")!!
mapType = processingEnv.erasedType("java.util.Map")!!
- ignoredTypes = IGNORED_TYPES.mapNotNull { processingEnv.erasedType(it) }
+ ignoredSuperTypes = IGNORED_SUPER_TYPES.mapNotNull { processingEnv.erasedType(it) }
+ ignoredExactTypes = IGNORED_EXACT_TYPES.mapNotNull { processingEnv.erasedType(it) }
}
override fun process(
@@ -109,7 +121,7 @@ class ImmutabilityProcessor : AbstractProcessor() {
classType: Symbol.TypeSymbol,
parentPolicyExceptions: Set<Immutable.Policy.Exception>,
): Boolean {
- if (classType.getAnnotation(Immutable.Ignore::class.java) != null) return false
+ if (isIgnored(classType)) return false
val policyAnnotation = classType.getAnnotation(Immutable.Policy::class.java)
val newPolicyExceptions = parentPolicyExceptions + policyAnnotation?.exceptions.orEmpty()
@@ -131,7 +143,7 @@ class ImmutabilityProcessor : AbstractProcessor() {
.fold(false) { anyError, field ->
if (field.isStatic) {
if (!field.isPrivate) {
- var finalityError = !field.modifiers.contains(Modifier.FINAL)
+ val finalityError = !field.modifiers.contains(Modifier.FINAL)
if (finalityError) {
printError(parentChain, field, MessageUtils.staticNonFinalFailure())
}
@@ -177,8 +189,10 @@ class ImmutabilityProcessor : AbstractProcessor() {
val newChain = parentChain + "$classType"
val hasMethodError = filteredElements
+ .asSequence()
.filter { it.getKind() == ElementKind.METHOD }
.map { it as Symbol.MethodSymbol }
+ .filterNot { it.isStatic }
.filterNot { IGNORED_METHODS.contains(it.name.toString()) }
.fold(false) { anyError, method ->
// Must call visitMethod first so it doesn't get short circuited by the ||
@@ -208,6 +222,14 @@ class ImmutabilityProcessor : AbstractProcessor() {
}
}
+ // Check all of the super classes, since methods in those classes are also accessible
+ (classType as? Symbol.ClassSymbol)?.run {
+ (interfaces + superclass).forEach {
+ val element = it.asElement() ?: return@forEach
+ visitClass(parentChain, seenTypesByPolicy, element, element, newPolicyExceptions)
+ }
+ }
+
if (isRegularClass && !anyError && allowFinalClassesFinalFields &&
!classType.modifiers.contains(Modifier.FINAL)
) {
@@ -301,16 +323,14 @@ class ImmutabilityProcessor : AbstractProcessor() {
parentPolicyExceptions: Set<Immutable.Policy.Exception>,
nonInterfaceClassFailure: () -> String = { MessageUtils.nonInterfaceReturnFailure() },
): Boolean {
+ if (isIgnored(symbol)) return false
+ if (isIgnored(type)) return false
if (type.isPrimitive) return false
if (type.isPrimitiveOrVoid) {
printError(parentChain, symbol, MessageUtils.voidReturnFailure())
return true
}
- if (ignoredTypes.any { processingEnv.typeUtils.isAssignable(type, it) }) {
- return false
- }
-
val policyAnnotation = symbol.getAnnotation(Immutable.Policy::class.java)
val newPolicyExceptions = parentPolicyExceptions + policyAnnotation?.exceptions.orEmpty()
@@ -357,16 +377,38 @@ class ImmutabilityProcessor : AbstractProcessor() {
message: String,
) = processingEnv.messager.printMessage(
Diagnostic.Kind.ERROR,
- // Drop one from the parent chain so that the directly enclosing class isn't logged.
- // It exists in the list at this point in the traversal so that further children can
- // include the right reference.
- parentChain.dropLast(1).joinToString() + "\n\t" + message,
+ parentChain.plus(element.simpleName).joinToString() + "\n\t " + message,
element,
)
private fun ProcessingEnvironment.erasedType(typeName: String) =
elementUtils.getTypeElement(typeName)?.asType()?.let(typeUtils::erasure)
- private fun isIgnored(symbol: Symbol) =
- symbol.getAnnotation(Immutable.Ignore::class.java) != null
+ private fun isIgnored(type: Type) =
+ (type.getAnnotation(Immutable.Ignore::class.java) != null)
+ || (ignoredSuperTypes.any { type.isAssignable(it) })
+ || (ignoredExactTypes.any { type.isSameType(it) })
+
+ private fun isIgnored(symbol: Symbol) = when {
+ // Anything annotated as @Ignore is always ignored
+ symbol.getAnnotation(Immutable.Ignore::class.java) != null -> true
+ // Then ignore exact types, regardless of what kind they are
+ ignoredExactTypes.any { symbol.type.isSameType(it) } -> true
+ // Then only allow methods through, since other types (fields) are usually a failure
+ symbol.getKind() != ElementKind.METHOD -> false
+ // Finally, check for any ignored super types
+ else -> ignoredSuperTypes.any { symbol.type.isAssignable(it) }
+ }
+
+ private fun TypeMirror.isAssignable(type: TypeMirror) = try {
+ processingEnv.typeUtils.isAssignable(this, type)
+ } catch (ignored: Exception) {
+ false
+ }
+
+ private fun TypeMirror.isSameType(type: TypeMirror) = try {
+ processingEnv.typeUtils.isSameType(this, type)
+ } catch (ignored: Exception) {
+ false
+ }
}
diff --git a/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt b/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
index f26357ff8e4b..2f7d59a7f0e5 100644
--- a/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
+++ b/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
@@ -90,7 +90,7 @@ class ImmutabilityProcessorTest {
@Test
fun validInterface() = test(
- JavaFileObjects.forSourceString(
+ source = JavaFileObjects.forSourceString(
"$PACKAGE_PREFIX.$DATA_CLASS_NAME",
/* language=JAVA */ """
package $PACKAGE_PREFIX;
@@ -227,49 +227,114 @@ class ImmutabilityProcessorTest {
nonInterfaceReturnFailure(line = 9),
nonInterfaceReturnFailure(line = 10, index = 0),
classNotFinalFailure(line = 13, "NonFinalClassFinalFields"),
- ), otherErrors = listOf(
- memberNotMethodFailure(line = 4) to FINAL_CLASSES[1],
- memberNotMethodFailure(line = 4) to FINAL_CLASSES[3],
+ ), otherErrors = mapOf(
+ FINAL_CLASSES[1] to listOf(
+ memberNotMethodFailure(line = 4),
+ ),
+ FINAL_CLASSES[3] to listOf(
+ memberNotMethodFailure(line = 4),
+ ),
)
)
+ @Test
+ fun superClass() {
+ val superClass = JavaFileObjects.forSourceString(
+ "$PACKAGE_PREFIX.SuperClass",
+ /* language=JAVA */ """
+ package $PACKAGE_PREFIX;
+
+ import java.util.List;
+
+ public interface SuperClass {
+ InnerClass getInnerClassOne();
+
+ final class InnerClass {
+ public String innerField;
+ }
+ }
+ """.trimIndent()
+ )
+
+ val dataClass = JavaFileObjects.forSourceString(
+ "$PACKAGE_PREFIX.$DATA_CLASS_NAME",
+ /* language=JAVA */ """
+ package $PACKAGE_PREFIX;
+
+ import java.util.List;
+
+ @Immutable
+ public interface $DATA_CLASS_NAME extends SuperClass {
+ String[] getArray();
+ }
+ """.trimIndent()
+ )
+
+ test(
+ sources = arrayOf(superClass, dataClass),
+ fileToErrors = mapOf(
+ superClass to listOf(
+ classNotImmutableFailure(line = 5, className = "SuperClass"),
+ nonInterfaceReturnFailure(line = 6),
+ nonInterfaceClassFailure(8),
+ classNotImmutableFailure(line = 8, className = "InnerClass"),
+ memberNotMethodFailure(line = 9),
+ ),
+ dataClass to listOf(
+ arrayFailure(line = 7),
+ )
+ )
+ )
+ }
+
private fun test(
source: JavaFileObject,
errors: List<CompilationError>,
- otherErrors: List<Pair<CompilationError, JavaFileObject>> = emptyList(),
+ otherErrors: Map<JavaFileObject, List<CompilationError>> = emptyMap(),
+ ) = test(
+ sources = arrayOf(source),
+ fileToErrors = otherErrors + (source to errors),
+ )
+
+ private fun test(
+ vararg sources: JavaFileObject,
+ fileToErrors: Map<JavaFileObject, List<CompilationError>> = emptyMap(),
) {
val compilation = javac()
.withProcessors(ImmutabilityProcessor())
- .compile(FINAL_CLASSES + ANNOTATION + listOf(source))
- val allErrors = otherErrors + errors.map { it to source }
- allErrors.forEach { (error, file) ->
- try {
- assertThat(compilation)
- .hadErrorContaining(error.message)
- .inFile(file)
- .onLine(error.line)
- } catch (e: AssertionError) {
- // Wrap the exception so that the line number is logged
- val wrapped = AssertionError("Expected $error, ${e.message}").apply {
- stackTrace = e.stackTrace
- }
+ .compile(FINAL_CLASSES + ANNOTATION + sources)
+
+ fileToErrors.forEach { (file, errors) ->
+ errors.forEach { error ->
+ try {
+ assertThat(compilation)
+ .hadErrorContaining(error.message)
+ .inFile(file)
+ .onLine(error.line)
+ } catch (e: AssertionError) {
+ // Wrap the exception so that the line number is logged
+ val wrapped = AssertionError("Expected $error, ${e.message}").apply {
+ stackTrace = e.stackTrace
+ }
- // Wrap again with Expect so that all errors are reported. This is very bad code
- // but can only be fixed by updating compile-testing with a better Truth Subject
- // implementation.
- expect.that(wrapped).isNull()
+ // Wrap again with Expect so that all errors are reported. This is very bad code
+ // but can only be fixed by updating compile-testing with a better Truth Subject
+ // implementation.
+ expect.that(wrapped).isNull()
+ }
}
}
- try {
- assertThat(compilation).hadErrorCount(allErrors.size)
- } catch (e: AssertionError) {
+ expect.that(compilation.errors().size).isEqualTo(fileToErrors.values.sumOf { it.size })
+
+ if (expect.hasFailures()) {
expect.withMessage(
compilation.errors()
+ .sortedBy { it.lineNumber }
.joinToString(separator = "\n") {
"${it.lineNumber}: ${it.getMessage(Locale.ENGLISH)?.trim()}"
}
- ).that(e).isNull()
+ ).fail()
}
}
@@ -307,4 +372,4 @@ class ImmutabilityProcessorTest {
val line: Long,
val message: String,
)
-} \ No newline at end of file
+}